Skip to main content
Guides

How to Convert requirements.txt to pyproject.toml

By Mithun··6 min read
Quick Answer

To convert a requirements.txt to pyproject.toml, put each requirement under [project].dependencies (PEP 621) and your dev tools under [dependency-groups].dev (PEP 735), keeping extras, version pins and environment markers verbatim. Then run uv lock to resolve and pin. Installation-only lines — --hash, --index-url, -c constraints — aren't dependencies and must be handled separately.

A requirements.txt lists what to install; a pyproject.toml *declares your project*, and it's what modern tools — uv, Hatch, pip — read first. Moving between them is mostly mechanical, but there's one trap: a requirements file mixes dependency declarations with installation instructions, and only the declarations belong in pyproject.

Here's the exact mapping, what carries over and what doesn't, and how it lines up with what uv produces — I ran the conversion both by hand and through uv 0.12.1 to check. You can do the whole thing in the browser with the requirements → pyproject converter.

The mapping

Runtime requirements go under [project].dependencies (PEP 621). Development-only tools go under [dependency-groups].dev (PEP 735) — a standardized group that's excluded from your built package's metadata, so it won't be installed by people who just pip install your project. Given these two files:

# requirements.txt
requests>=2.31,<3
flask[async]==3.0.1
numpy>=1.26 ; python_version >= "3.10"
click~=8.1
# requirements-dev.txt
pytest>=8
ruff
requirements.txt (runtime) + requirements-dev.txt

…the conversion is this. Each line becomes a PEP 508 dependency string, extras ([async]), specifiers (~=8.1) and markers all kept as-is:

[project]
name = "demo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"click~=8.1",
"flask[async]==3.0.1",
"numpy>=1.26 ; python_version >= '3.10'",
"requests>=2.31,<3",
]
[dependency-groups]
dev = [
"pytest>=8",
"ruff",
]
pyproject.toml

This matches what uv add -r requirements.txt and uv add --dev -r requirements-dev.txt write — I verified it against uv 0.12.1. One nuance: uv normalizes the marker python_version to python_full_version when it rewrites the file; the browser converter preserves your original marker instead (both are valid — see below).

What converts, and what doesn't

The declarations convert cleanly. The installation instructions don't — and an output can be valid TOML while quietly failing to reproduce your original install. Know the difference:

Requirements lineConverts to pyproject?Why
name, ==/>=/~= specifiersYesKept verbatim in the dependency string.
extras, e.g. flask[async]YesPart of the PEP 508 string.
environment markers ; python_version …YesPreserved exactly (see the marker note).
direct URL, name @ https://…YesPEP 508 supports direct references.
--hash=…No — reportedpyproject deps carry no hashes; hash verification of the downloaded artifact is lost. Use a lockfile.
--index-url / --extra-index-urlNo — reportedChanges where packages come from; belongs in uv/pip config, not pyproject.
-c constraints.txtNo — reportedConstraints restrict versions without adding deps; they can’t become dependencies.
-r other.txtNo — reportedA nested file isn’t loaded; add its contents to the matching input.
-e . / -e ./pkg (editable)No — manualNeeds a path/VCS source; -e . is the project itself, never a self-dependency.

Dropping --hash silently removes artifact-hash verification, and ignoring a -c constraints file can permit versions the original forbade. Treat those as an incomplete conversion and reapply them — don't assume valid TOML means a faithful install.

On markers: don’t rewrite python_version

A tempting "cleanup" is to swap python_version for python_full_version. Don't do it blindly — they're different variables. python_version is the two-component X.Y (e.g. 3.10); python_full_version is the full X.Y.Z (e.g. 3.10.4). For a marker like >= "3.10" they usually agree, but for exact or upper-bound comparisons they don't, and a mechanical replacement can change which interpreters match. Preserve the marker you were given; let a tool normalize it only when it also understands the comparison.

Finish with uv: conversion isn’t resolution

Converting the file doesn't pick versions. Your pins and ranges are carried over, but nothing is resolved into a single, locked set until you run a resolver. After you've saved pyproject.toml:

uv lock # resolve every dependency into uv.lock
uv sync # create the virtual environment from the lock
resolve + install

Or skip the hand-conversion entirely and let uv build the project from your requirements — note it resolves versions and may add its own bounds, so the result can differ from a literal conversion:

uv init --bare
uv add -r requirements.txt
uv add --dev -r requirements-dev.txt
let uv do it

Common gotchas

  • pip freeze output isn't a dependency list. A frozen file pins *every* installed package, including transitive ones. Convert it if you like, but review which packages your project actually depends on directly — the rest belong to the resolver, not your [project].dependencies.
  • requirements.in vs a compiled requirements.txt. A requirements.in (your hand-written direct deps) is the better source to convert; a pip-tools/uv-*compiled* requirements.txt is a lockfile-like artifact — prefer letting uv lock regenerate that from pyproject.
  • Dev extras vs dev groups. Old projects sometimes put dev tools in [project.optional-dependencies] (an extras). PEP 735 [dependency-groups] is the current home for dev-only tools that shouldn't ship in your package metadata.

Do all of this in the browser — including the unresolved-items report — with the requirements → pyproject converter. For uv itself, see uv, the fast Python package manager.

Convert your requirements.txt now

Paste requirements.txt (and dev requirements) and get a pyproject.toml, with an unresolved-items report — in your browser.

Open the converter

Free tools mentioned here

Related guides

Frequently asked questions

Where do runtime vs dev dependencies go in pyproject.toml?

Runtime dependencies go in [project].dependencies (PEP 621). Development-only tools (pytest, ruff, mypy) go in [dependency-groups].dev (PEP 735), a standardized group excluded from your built package's metadata.

Does converting to pyproject.toml pin my versions?

No. Conversion carries over your pins and ranges but doesn't resolve them into a single locked set. Run 'uv lock' (then 'uv sync') to resolve and pin every dependency into uv.lock.

What happens to --hash, --index-url and constraints (-c)?

None of them are dependencies, so they don't convert. --hash provides artifact verification, --index-url sets where packages come from, and -c restricts versions — reapply them via a lockfile or uv/pip config. A converter should report them rather than drop them silently.

Should I convert a pip freeze file?

You can, but a frozen file lists transitive packages too. Keep your pins if you want reproducibility, but review which packages your project directly depends on — the transitive ones belong to the resolver, not [project].dependencies.

Is it safe to replace python_version with python_full_version?

Not blindly. python_version is X.Y and python_full_version is X.Y.Z; for exact or upper-bound comparisons they behave differently, so a mechanical swap can change which interpreters match. Preserve the original marker.

Keep reading