šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Understanding pyproject.toml

The single file that replaced setup.py, setup.cfg, requirements.txt, and half a dozen tool-specific config files — read and write pyproject.toml with confidence.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What does the [build-system] table specify?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

pyproject.toml, standardized across PEP 517, 518, and 621, is now the single source of truth for a Python project's build system, dependencies, and metadata — and doubles as the shared configuration file for tools like ruff, mypy, and pytest. This lesson teaches you to read and write one fluently.

1[build-system]: How to Build, Before What to Build

Before pip can install your project from source, it needs to know *how* to turn your source tree into an installable distribution — that's a separate question from what your project's own dependencies are, and it's answered by the [build-system] table, standardized by PEP 517 and PEP 518. requires = ["hatchling"] declares which build backend package pip should install (in an isolated environment) to perform the build; build-backend = "hatchling.build" tells it which importable object in that package implements the actual build interface.

This might look like unnecessary indirection, but it's what makes the Python packaging ecosystem backend-agnostic: hatchling, setuptools, poetry-core, flit-core, and others all implement the same PEP 517 interface, so any PEP 517-compliant tool (pip, build, uv) can build any project correctly without needing backend-specific logic hardcoded into it. Before this standard existed, every project had an executable setup.py that pip had to actually run to figure out how to build it — fragile, and a real security concern, since running arbitrary code from an untrusted package just to inspect it is inherently risky.

For most new projects, you pick one build backend (Hatchling and setuptools are both common, mature choices) once at project creation, and rarely think about [build-system] again — it's boilerplate you copy once, not something you hand-tune per project.

āœ•
—
+
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
localhost:3000
Build Resolution
[build-system]
Declares HOW to build — resolved before your project's own deps

2[project]: Metadata That Replaced setup.py

[project], standardized by PEP 621, holds exactly the metadata that used to require an executable setup.py calling setup(name=..., version=..., install_requires=[...]) — but as static, declarative TOML data instead of executable Python code. name, version, requires-python (the minimum/range of supported Python versions), and dependencies (the runtime dependency list, equivalent to the old install_requires) are the fields you'll touch most often.

Declaring dependencies statically here, rather than in a separate requirements.txt, means your project's dependency *requirements* (what it needs, expressed as version ranges like httpx>=0.27) live in the same file as everything else about the project. It's important to distinguish this from a *lock file* (like uv.lock or poetry.lock, covered in the Dependency Management lesson) — [project.dependencies] says what's acceptable, a lock file pins exactly what's actually installed, down to the specific version and hash.

requires-python = ">=3.11" is more than documentation — build tools and installers actively enforce it, refusing to install your package on an interpreter version outside that range, catching a genuine incompatibility (using a 3.11-only syntax feature, for instance) at install time rather than as a confusing runtime SyntaxError for a user on an older interpreter.

āœ•
—
+
[project]
name = "my-package"
version = "1.2.0"
requires-python = ">=3.11"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.0",
]
localhost:3000
Declarative Metadata
[project]
name, version, dependencies, requires-python — all static TOML, no executable setup.py

3Optional Dependencies and Shared Tool Configuration

[project.optional-dependencies] defines named groups of dependencies that are *not* installed by default — dev = ["pytest>=8.0", "ruff>=0.5", "mypy>=1.10"] means a plain pip install my-package skips these entirely, while pip install -e ".[dev]" installs the base package plus everything in the dev group. This is the standard mechanism for separating what end users of a published library actually need at runtime from what contributors need to develop and test it — keeping a production install lean while still declaring dev tooling requirements in one canonical place.

Beyond project metadata, pyproject.toml has become the de facto shared configuration file for the broader tooling ecosystem too, via [tool.X] tables — [tool.ruff], [tool.mypy], [tool.pytest.ini_options], [tool.black] (though Black has increasingly deferred to Ruff's formatter in many projects) all read their settings from here, each tool ignoring every other tool's table. This consolidation is a genuine ergonomic win over the previous era of one dotfile per tool (.flake8, setup.cfg, pytest.ini, mypy.ini) scattered across the project root.

The practical result: a new contributor cloning a well-configured modern Python project can understand nearly everything about how it's built, what it depends on, and how it's linted/typed/tested by reading a single file top to bottom — a meaningful improvement in discoverability over the fragmented, multi-file configuration Python projects historically required.

āœ•
—
+
[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.5", "mypy>=1.10"]

# Install with:
# pip install -e ".[dev]"
localhost:3000
Consolidated Config
[tool.ruff], [tool.mypy], [tool.pytest.ini_options]
One file instead of four+ dotfiles

4Step-by-Step Breakdown

One TOML file now does what used to take setup.py, setup.cfg, requirements.txt, MANIFEST.in, and a handful of dotfiles. Let's learn its anatomy.

[build-system] tells pip HOW to build your package before it even looks at your code — which tool to invoke.

Checkpoint: What does the [build-system] table specify?

  • →Which tool (build backend) should be used to build the package, before pip even looks at your source
  • →The package's runtime dependencies needed to actually run the code

[project] holds the metadata that used to live in setup.py's setup() call — name, version, dependencies, Python version support.

Optional dependency groups — like [project.optional-dependencies] dev — let you install extras only when needed.

Checkpoint: What does pip install -e ".[dev]" install, compared to pip install -e .?

  • →The base package plus the optional "dev" dependency group
  • →Exactly the same thing — [dev] has no effect

Tool configuration lives in [tool.X] sections — ruff, mypy, and pytest can all be configured in this one file instead of separate dotfiles.

With the project's metadata and build system declared, dependency management is the next layer — how those dependencies get resolved, locked, and installed.

Parse a Real pyproject.toml. Finish get_dependencies(): tomllib parses TOML into a plain nested dict.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Pin requires-python accurately and let build tools enforce it

A precise requires-python (e.g. ">=3.11") turns a version incompatibility into a clear install-time error instead of a confusing runtime SyntaxError or AttributeError for users on an unsupported interpreter.

Separate runtime dependencies from dev/test tooling via optional-dependencies groups

Keeping [project.dependencies] lean (only what end users need at runtime) while dev tools live under an extras group like [dev] keeps production installs fast and minimal.

Frequent Bugs

THE BUG

Adding a test-only or lint-only dependency (like pytest or ruff) directly to [project.dependencies] instead of an optional-dependencies group, forcing every end user to install dev tooling they'll never use.

THE FIX

Move dev/test/lint tooling into [project.optional-dependencies] under a group like dev, and install it locally with pip install -e ".[dev]" — keep [project.dependencies] limited to genuine runtime requirements.

Real-World Examples

A Complete Minimal pyproject.toml for a New Library

A team is bootstrapping a new internal library and needs a pyproject.toml covering the build system, metadata, runtime dependencies, dev extras, and shared tool configuration in one file.

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "acme-toolkit"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27"]

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.5", "mypy>=1.10"]

[tool.ruff]
line-length = 100

[tool.pytest.ini_options]
testpaths = ["tests"]

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Forgetting the [build-system] table entirely, causing pip install -e . to fail because it doesn't know which backend to use to build the project.

# Wrong: missing entirely, pip install fails [project] name = "my-package" version = "0.1.0" # Correct [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "my-package" version = "0.1.0"

The Solution //

Always include a [build-system] table with requires and build-backend, even for the simplest project — most build backends' documentation gives you the exact two lines to copy.

Lesson Glossary

[01]pyproject.toml

The standardized, TOML-format file (PEP 517/518/621) declaring a Python project's build system, metadata, dependencies, and tool configuration.

Code Preview
// pyproject.toml context

[02]Build backend

The tool (e.g. hatchling, setuptools) responsible for turning a source tree into an installable distribution, declared under [build-system].

Code Preview
// Build backend context

[03]Optional dependency group

A named set of extra dependencies (under [project.optional-dependencies]) not installed by default, requested via package[group-name].

Code Preview
// Optional dependency group context

[04][tool.X] table

A namespaced TOML table in pyproject.toml where a specific tool (ruff, mypy, pytest) reads its own configuration, ignored by all other tools.

Code Preview
// [tool.X] table context

Continue Learning