{"id":117233,"date":"2026-06-19T22:43:30","date_gmt":"2026-06-19T17:13:30","guid":{"rendered":"https:\/\/www.guvi.in\/blog\/?p=117233"},"modified":"2026-06-19T22:43:32","modified_gmt":"2026-06-19T17:13:32","slug":"python-packaging-with-pyproject-toml","status":"publish","type":"post","link":"https:\/\/www.guvi.in\/blog\/python-packaging-with-pyproject-toml\/","title":{"rendered":"Python Packaging with pyproject.toml Explained"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>Quick TL;DR&nbsp;<\/strong><\/h2>\n\n\n\n<ul>\n<li>pyproject.toml is the modern, PEP-standardized configuration file that replaces setup.py and setup.cfg for Python packaging. <\/li>\n\n\n\n<li>It centralizes build system declaration, project metadata, and tool configuration in a single file \u2014 making projects cleaner, more portable, and easier to maintain. <\/li>\n\n\n\n<li>This guide covers basic structure, dependency management, build backends, and publishing to PyPI.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Introduction<\/strong><\/h2>\n\n\n\n<p>Python packaging has historically been fragmented \u2014 setup.py, setup.cfg, MANIFEST.in, tox.ini, and more scattered across a project root. pyproject.toml changes that. Introduced through PEP 518 and expanded by PEP 621, it provides a single, standardized configuration surface that modern tools \u2014 pip, build, Poetry, Hatch, PDM \u2014 all understand natively.<\/p>\n\n\n\n<p>If you are still using a bare setup.py in 2026, this guide is the upgrade path you need.<\/p>\n\n\n\n<p><em>Ready to move beyond configuration and build production-grade Python applications? Explore <strong>HCL GUVI&#8217;s <\/strong><a href=\"https:\/\/www.guvi.in\/zen-class\/python-course\/?utm_source=blog&amp;utm_medium=hyperlink&amp;utm_campaign=python-packaging-with-pyproject-toml+\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Python Course<\/strong><\/a>  structured learning that takes you from core fundamentals to real-world projects, with mentorship and placement support built in.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The Core Structure of pyproject.toml<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>A minimal but complete pyproject.toml:\n\n&#91;build-system]\n\nrequires = &#91;\"hatchling\"]\n\nbuild-backend = \"hatchling.build\"\n\n&#91;project]\n\nname = \"my-package\"\n\nversion = \"0.1.0\"\n\ndescription = \"A short description of the project\"\n\nreadme = \"README.md\"\n\nrequires-python = \"&gt;=3.10\"\n\nlicense = { text = \"MIT\" }\n\nauthors = &#91;\n\n&nbsp;&nbsp;{ name = \"Your Name\", email = \"you@example.com\" }\n\n]\n\ndependencies = &#91;\n\n&nbsp;&nbsp;\"requests&gt;=2.28\",\n\n&nbsp;&nbsp;\"httpx&gt;=0.24\"\n\n]\n\n&#91;project.urls]\n\nHomepage = \"https:\/\/github.com\/yourname\/my-package\"<\/code><\/pre>\n\n\n\n<p>Every section has a specific purpose \u2014 nothing is implicit, nothing executes at parse time.<\/p>\n\n\n\n<p><strong>Read More: <\/strong><a href=\"https:\/\/www.guvi.in\/blog\/packaging-your-python-code-for-production\/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>\u00a0Packaging Your Python Code for Production: Virtualenvs, setup.py &amp; Publishing<\/strong><\/a><\/p>\n\n\n\n<div style=\"background-color: #099f4e; border: 3px solid #110053; border-radius: 12px; padding: 18px 22px; color: #FFFFFF; font-family: Montserrat, Helvetica, sans-serif; line-height: 1.6; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); max-width: 800px;\">\n  <strong style=\"font-size: 22px; color: #FFFFFF;\">\ud83d\udca1 Did You Know?<\/strong>\n  <p style=\"margin-top: 14px;\">\n    <strong>PEP 518<\/strong>, which introduced <code>pyproject.toml<\/code> as the standard configuration file for Python builds, was accepted in 2016, but widespread ecosystem adoption took several years to materialize. A major turning point came with modern packaging tooling\u2014particularly updates in <strong>pip 19.0<\/strong>\u2014which helped establish <code>pyproject.toml<\/code> as a central interface for build system configuration in 2019. Over time, tools such as setuptools, Poetry, and other build backends converged around this standard. By 2026, <code>pyproject.toml<\/code> has effectively become the authoritative configuration format across the Python packaging ecosystem, simplifying dependency management and build consistency across projects.\n  <\/p>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Breaking Down Each Table<\/strong><\/h2>\n\n\n\n<ol>\n<li><strong>[build-system]<\/strong><\/li>\n<\/ol>\n\n\n\n<p>Mandatory. Declares the <a href=\"https:\/\/www.guvi.in\/blog\/what-are-python-packages\/\" target=\"_blank\" rel=\"noreferrer noopener\">packages<\/a> pip must install into an isolated build environment (requires) and the backend that performs the build (build-backend).<\/p>\n\n\n\n<p>Common backend choices in 2026:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><strong>Backend<\/strong><\/td><td><strong>Package<\/strong><\/td><td><strong>Best For<\/strong><\/td><\/tr><tr><td>hatchling<\/td><td>hatchling<\/td><td>General-purpose, fast<\/td><\/tr><tr><td>setuptools<\/td><td>setuptools<\/td><td>Legacy compatibility<\/td><\/tr><tr><td>flit-core<\/td><td>flit<\/td><td>Pure Python, minimal<\/td><\/tr><tr><td>poetry-core<\/td><td>poetry-core<\/td><td>Poetry-managed projects<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<ol start=\"2\">\n<li><strong>[project]<\/strong><\/li>\n<\/ol>\n\n\n\n<p>The PEP 621 metadata table. Key fields \u2014 name, version, requires-python, dependencies, and optional-dependencies for extras users install explicitly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;project.optional-dependencies]\n\ndev = &#91;\"pytest&gt;=7\", \"ruff\", \"mypy\"]\n\ndocs = &#91;\"mkdocs\", \"mkdocs-material\"]\n\nInstall with extras: pip install my-package&#91;dev]<\/code><\/pre>\n\n\n\n<ol start=\"3\">\n<li><strong>Dynamic Metadata<\/strong><\/li>\n<\/ol>\n\n\n\n<p>To avoid version drift between code and package metadata, declare version as dynamic:<\/p>\n\n\n\n<p>[project]<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>name = \"my-package\"\n\ndynamic = &#91;\"version\"]\n\n&#91;tool.hatch.version]\n\npath = \"src\/my_package\/__init__.py\"<\/code><\/pre>\n\n\n\n<ol start=\"4\">\n<li><strong>[project.scripts]<\/strong><\/li>\n<\/ol>\n\n\n\n<p>Defines CLI entry points \u2014 pip creates executable wrappers automatically on install:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;project.scripts]\n\nmy-cli = \"my_package.cli:main\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Tool Configuration and Project Layout<\/strong><\/h2>\n\n\n\n<ol>\n<li><strong>Consolidating Tool Config<\/strong><\/li>\n<\/ol>\n\n\n\n<p>Tools that previously required separate config files now live under [tool.&lt;name&gt;]:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;tool.pytest.ini_options]\n\ntestpaths = &#91;\"tests\"]\n\naddopts = \"-v --tb=short\"\n\n&#91;tool.mypy]\n\nstrict = true\n\npython_version = \"3.11\"\n\n&#91;tool.ruff]\n\nline-length = 88\n\nselect = &#91;\"E\", \"F\", \"I\"]<\/code><\/pre>\n\n\n\n<p>This eliminates pytest.ini, mypy.ini, and .flake8 \u2014 significantly reducing root-level clutter.<\/p>\n\n\n\n<ol start=\"2\">\n<li><strong>Recommended src Layout<\/strong><strong><br><\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>my-package\/\n\n\u251c\u2500\u2500 src\/\n\n\u2502 &nbsp; \u2514\u2500\u2500 my_package\/\n\n\u2502 &nbsp; &nbsp; &nbsp; \u251c\u2500\u2500 __init__.py\n\n\u2502 &nbsp; &nbsp; &nbsp; \u2514\u2500\u2500 core.py\n\n\u251c\u2500\u2500 tests\/\n\n\u2502 &nbsp; \u2514\u2500\u2500 test_core.py\n\n\u251c\u2500\u2500 pyproject.toml\n\n\u2514\u2500\u2500 README.md<\/code><\/pre>\n\n\n\n<p>With Hatchling, the src layout is auto-detected. With setuptools, declare it explicitly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;tool.setuptools.packages.find]\n\nwhere = &#91;\"src\"]<\/code><\/pre>\n\n\n\n<p>The src layout prevents accidental imports of local source instead of the installed package \u2014 a subtle but real issue in flat layouts.<\/p>\n\n\n\n<p>Ready to move beyond configuration and build production-grade Python applications? Explore <strong>HCL GUVI&#8217;s <\/strong><a href=\"https:\/\/www.guvi.in\/zen-class\/python-course\/?utm_source=blog&amp;utm_medium=hyperlink&amp;utm_campaign=python-packaging-with-pyproject-toml+\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Python Course<\/strong><\/a> structured learning that takes you from core fundamentals to real-world projects, with mentorship and placement support built in.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Building, Publishing, and Migration<\/strong><\/h2>\n\n\n\n<ol>\n<li><strong>Building and Publishing<\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install build twine\n\npython -m build\n\ntwine upload dist\/*<\/code><\/pre>\n\n\n\n<p>The build tool reads [build-system] automatically. For TestPyPI validation before a real release: twine upload &#8211;repository testpypi dist\/*<\/p>\n\n\n\n<ol start=\"2\">\n<li><strong>Migration from setup.py<\/strong><\/li>\n<\/ol>\n\n\n\n<p><strong>Before:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from setuptools import setup, find_packages\n\nsetup(\n\n&nbsp;&nbsp;&nbsp;&nbsp;name=\"my-package\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;version=\"0.1.0\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;install_requires=&#91;\"requests\"],\n\n&nbsp;&nbsp;&nbsp;&nbsp;packages=find_packages(),\n\n)<\/code><\/pre>\n\n\n\n<p><strong>After:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;build-system]\n\nrequires = &#91;\"setuptools&gt;=68\", \"wheel\"]\n\nbuild-backend = \"setuptools.backends.legacy:build\"\n\n&#91;project]\n\nname = \"my-package\"\n\nversion = \"0.1.0\"\n\ndependencies = &#91;\"requests\"]\n\n&#91;tool.setuptools.packages.find]\n\nwhere = &#91;\".\"]<\/code><\/pre>\n\n\n\n<p>This retains setuptools as the backend \u2014 no changes to existing <a href=\"https:\/\/www.guvi.in\/blog\/understanding-ci-cd\/\" target=\"_blank\" rel=\"noreferrer noopener\">CI\/CD pipelines<\/a> \u2014 while adopting the modern configuration format.<\/p>\n\n\n\n<div style=\"background-color: #099f4e; border: 3px solid #110053; border-radius: 12px; padding: 18px 22px; color: #FFFFFF; font-family: Montserrat, Helvetica, sans-serif; line-height: 1.6; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); max-width: 800px;\">\n  <strong style=\"font-size: 22px; color: #FFFFFF;\">\ud83d\udca1 Did You Know?<\/strong>\n  <p style=\"margin-top: 14px;\">\n    According to the <strong>Python Developers Survey 2024<\/strong>, over <strong>68% of Python developers<\/strong> now use <code>pyproject.toml<\/code> as their primary project configuration file, a significant increase from just 29% in 2021. This shift reflects the broader adoption of modern Python packaging standards introduced through <strong>PEPs like 518 and 621<\/strong>. The change has been especially strong among <strong>open-source library maintainers<\/strong>, where cleaner and more standardized packaging improves contributor onboarding, dependency management, and overall project portability. As the ecosystem continues to mature, <code>pyproject.toml<\/code> has become the default foundation for Python project configuration and build tooling.\n  <\/p>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Common Mistakes to Avoid<\/strong><\/h2>\n\n\n\n<p><strong>1. Mixing setup.py with pyproject.toml<\/strong> \u2014 A residual setup.py can cause confusing double-invocation behavior. Remove it once migration is complete.<\/p>\n\n\n\n<p><strong>2. Putting dev dependencies in [project.dependencies]<\/strong> \u2014 <a href=\"https:\/\/www.guvi.in\/blog\/best-software-development-tools\/\">Dev tools<\/a> like pytest and ruff should go in optional-dependencies or dependency-groups, not the main dependency list.<\/p>\n\n\n\n<p><strong>3. Hardcoding version in two places<\/strong> \u2014 Declare version as dynamic and derive it from one authoritative location. Keeping setup metadata and __version__ in sync manually is a maintenance liability.<\/p>\n\n\n\n<p><strong>4. Omitting requires-python<\/strong> \u2014 Without this, pip may install your package on an incompatible Python version and produce cryptic runtime errors.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Conclusion<\/strong><\/h2>\n\n\n\n<p>pyproject.toml is not a trend \u2014 it is the settled, PEP-backed standard for Python packaging in 2026. It consolidates build system declaration, project metadata, and tool configuration into a single file that every major Python tool understands.&nbsp;<\/p>\n\n\n\n<p>Whether starting fresh or migrating an existing project, adopting pyproject.toml reduces configuration sprawl, improves reproducibility, and aligns your project with the direction the ecosystem has already moved.<\/p>\n\n\n\n<p>Start with [build-system] and [project]  everything else layers on top.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>FAQ<\/strong><\/h2>\n\n\n<div id=\"rank-math-faq\" class=\"rank-math-block\">\n<div class=\"rank-math-list \">\n<div id=\"faq-question-1781753483047\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>What is pyproject.toml used for?<\/strong>\u00a0<\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Declaring the build system, defining project metadata such as name, version, and dependencies, and storing configuration for development tools like pytest, mypy, and ruff.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1781753489876\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>Is pyproject.toml a replacement for setup.py?<\/strong>\u00a0<\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Yes \u2014 for new projects, pyproject.toml with a modern backend like Hatchling or flit-core replaces both setup.py and setup.cfg entirely.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1781753501766\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>Which build backend should I choose?<\/strong>\u00a0<\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Hatchling is a solid default for new projects. Setuptools remains appropriate for existing projects with complex build logic.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1781753510100\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>Can I keep using setup.cfg alongside pyproject.toml?<\/strong>\u00a0<\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Not recommended. Migrate everything to pyproject.toml and delete setup.cfg to avoid metadata ambiguity.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1781753519007\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>What is the difference between dependencies and optional-dependencies?<\/strong>\u00a0<\/h3>\n<div class=\"rank-math-answer \">\n\n<p>dependencies install whenever your package installs. optional-dependencies are grouped extras users install explicitly \u2014 commonly used for dev tooling, docs, or feature extensions.<\/p>\n\n<\/div>\n<\/div>\n<\/div>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Quick TL;DR&nbsp; Introduction Python packaging has historically been fragmented \u2014 setup.py, setup.cfg, MANIFEST.in, tox.ini, and more scattered across a project root. pyproject.toml changes that. Introduced through PEP 518 and expanded by PEP 621, it provides a single, standardized configuration surface that modern tools \u2014 pip, build, Poetry, Hatch, PDM \u2014 all understand natively. If you [&hellip;]<\/p>\n","protected":false},"author":63,"featured_media":117762,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[717],"tags":[],"views":"24","authorinfo":{"name":"Vishalini Devarajan","url":"https:\/\/www.guvi.in\/blog\/author\/vishalini\/"},"thumbnailURL":"https:\/\/www.guvi.in\/blog\/wp-content\/uploads\/2026\/06\/python-packaging-with-pyproject-toml-300x115.webp","_links":{"self":[{"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/posts\/117233"}],"collection":[{"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/users\/63"}],"replies":[{"embeddable":true,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/comments?post=117233"}],"version-history":[{"count":3,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/posts\/117233\/revisions"}],"predecessor-version":[{"id":117763,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/posts\/117233\/revisions\/117763"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/media\/117762"}],"wp:attachment":[{"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/media?parent=117233"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/categories?post=117233"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.guvi.in\/blog\/wp-json\/wp\/v2\/tags?post=117233"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}