Python Tooling for Rust Developers
Cargo manages dependencies, builds, tests, and publishes crates, while rustfmt (cargo fmt), clippy (cargo clippy), and rustup handle formatting, linting, and toolchains. Python has no single tool with that scope, but uv comes closest. This article maps Rust tooling concepts to their Python counterparts so Rust developers can orient quickly.
What Will Feel Familiar, and What Will Not
Most Rust workflows have a recognizable Python equivalent:
| Rust | Python | Notes |
|---|---|---|
rustup |
uv python install |
Python version management |
| Cargo | uv | Closest single-tool equivalent |
Cargo.toml |
pyproject.toml |
Project metadata and dependencies |
Cargo.lock |
uv.lock |
Pinned dependency versions |
cargo add |
uv add |
Add a dependency |
cargo run |
uv run |
Run project code |
cargo test |
uv run pytest |
Run tests |
| clippy | Ruff | Linter |
| rustfmt | ruff format |
Code formatter |
| Compiler type checking | mypy / Pyright / ty | Optional, gradual |
cargo publish |
uv build + uv publish |
Build distribution artifacts and publish |
cargo build |
No direct equivalent | Python runs from source; no general app build step |
| crates.io | PyPI | Package registry |
Important
These analogies are orientation aids, not exact equivalences.
Two mental model gaps will affect daily work:
Gradual typing is a different contract. Python’s type system is optional: code with zero annotations runs without complaint, and a type checker is a separate tool, not the compiler, so it won’t catch everything rustc would. With no compilation step for pure Python (CPython compiles to bytecode internally, but there is no target/, no debug-vs-release profile, no link step), error detection shifts to runtime, so the feedback loop is faster but fewer bugs are caught before the code runs.
Standards and tools are separate. Where Rust has Cargo as the build system, Python’s PEPs define interfaces (declaring dependencies, building packages, specifying metadata) that multiple tools implement, which is why so many packaging tools exist. uv covers most of the surface, but knowing standards and tools are decoupled explains choices that otherwise seem arbitrary.
Python’s Three-Layer Model
Cargo unifies three things that Python separates: the runtime (interpreter version), the dependency sandbox (virtual environment), and the project definition (pyproject.toml).
Interpreter Management
uv python install 3.12 fills the role of rustup toolchain install stable, downloading and managing Python interpreters. Historically Python’s interpreter and dependency managers were separate tools; uv merges them, but the conceptual separation still matters. A virtual environment attaches to an existing interpreter and does not choose or install Python on its own.
The Virtual Environment
Where Cargo stores compiled artifacts in a per-project target/ directory, Python uses virtual environments to isolate installed packages per project. Each contains a link to a Python interpreter and its own site-packages directory for dependencies. Without one, all projects share the same global packages, leading to version conflicts.
uv creates and manages virtual environments automatically. Running uv sync or uv run ensures the environment exists and matches the project’s declared dependencies.
pyproject.toml
pyproject.toml is Python’s Cargo.toml. It declares the project name, version, dependencies, and Python version constraints. It also hosts configuration for other tools (ruff, mypy, pytest) in [tool.*] tables, as if clippy and rustfmt settings lived inside Cargo.toml.
Unlike Cargo.toml, the format is governed by multiple PEPs rather than one tool’s specification: PEP 621 defines the [project] table, while build-system config, dependency groups, and tool settings each have their own standards. This is part of why there are so many packaging tools: the standards define what goes in the file, and different tools implement the machinery behind it.
The Daily Development Loop
Adding and Managing Dependencies
uv add requests works like cargo add reqwest. It updates pyproject.toml and the lockfile in one step. uv sync installs everything from the lockfile, comparable to building from Cargo.lock.
Python has no equivalent of Cargo’s feature flags. Instead, libraries declare optional extras (under [project.optional-dependencies]) that consumers request at install time, such as uv add "httpx[http2]", while dependency groups (under [dependency-groups]) hold development, testing, or documentation dependencies. Extras are published package metadata; dependency groups are not.
Running Python Code and Import Semantics
uv run python main.py is the closest analog to cargo run. There is no build step; the interpreter reads and executes source files directly.
Python has two ways to run code that Rust has no equivalent for: python file.py runs a single script, while python -m package runs a package as a module, which affects how imports resolve. The distinction matters when a project uses relative imports.
Console scripts (entry points defined in pyproject.toml) serve a similar role to Cargo’s [[bin]] targets. They create named commands that invoke specific functions, installed into the environment’s bin/ directory.
Packages commonly put source code in a src/ directory, though flat layouts are also valid and widely used. During development, an editable install (uv sync handles this) lets changes to source files take effect immediately without reinstalling. Cargo handles this implicitly; Python requires the editable install mechanism because the interpreter needs packages to be “installed” to find them through its import system.
Code Quality: Linting and Formatting
Ruff combines linting and formatting into one tool, covering the territory of both clippy and rustfmt. ruff check lints, ruff format formats, and ruff check --fix auto-fixes. Configuration lives in pyproject.toml under [tool.ruff].
Ruff re-implements rules from dozens of older Python linting tools, so most projects need only this single dependency for code quality enforcement.
Type Checking
This is the biggest adjustment for Rust developers. Python’s type system is optional and gradual: code with zero annotations executes without issue. The type checker is a standalone tool that reads annotations and reports inconsistencies, but it is not required to run the code.
Even with full annotations, Python’s type system cannot express ownership, lifetimes, or borrow semantics. It catches a different class of bugs: wrong argument types, missing return values, attribute access on None, and similar logical errors. It will not prevent data races or guarantee memory safety.
Three type checkers are in active use: mypy, pyright, and ty. mypy is the original and most widely adopted, and pyright is the engine behind Pylance, VS Code’s default Python language server. ty is the newest, built by the team behind ruff and uv, and the fastest.
Testing with pytest
pytest is Python’s standard test runner. Like cargo test, it discovers tests by convention rather than requiring explicit registration. The conventions differ: pytest looks for files named test_*.py (and *_test.py) and functions named test_*, rather than functions annotated with #[test].
def test_addition():
assert 1 + 1 == 2That is a complete test. No imports, no decorators, no test class required. pytest rewrites assert statements to provide detailed failure messages.
pytest fixtures replace Rust’s setup/teardown patterns. A fixture is a function that provides test dependencies (database connections, temporary files, mock services) through dependency injection.
Tests declare which fixtures they need as function parameters, and pytest wires them up automatically.
def test_read_config(tmp_path):
config_file = tmp_path / "config.toml"
config_file.write_text('key = "value"')
assert read_config(config_file)["key"] == "value"Run tests with uv run pytest. Add -v for verbose output, -x to stop on the first failure.
Packaging and Distribution
Library Packaging
Where Rust crates compile on the consumer’s machine, Python packages come as source distributions (sdists) and prebuilt wheels. When a compatible wheel exists, installation skips the build step, which is why pip install numpy finishes in seconds instead of compiling C and Fortran; without one, the installer builds from the sdist. A build backend creates both formats.
Publishing to PyPI is the equivalent of cargo publish to crates.io. uv build creates the distribution files, and uv publish uploads them.
Application Deployment
Rust produces a compiled native binary (static vs. dynamic linkage depends on target and flags). Ship it and the job is done.
Python applications are deployed as source code plus an environment: a virtual environment with installed dependencies, typically inside a container image, or on a server with a process manager. There is no single-binary output by default.
For CLI tools, pipx install and uv tool install provide something like cargo install, creating an isolated environment per tool so that end users can run Python CLI applications without managing dependencies themselves. (uvx is uv tool run, which runs tools in temporary environments without permanent installation.) This is limited to the Python ecosystem; it does not produce standalone binaries.
Tools like PyInstaller (bundler) and Nuitka (Python-to-C compiler) can produce standalone executables, but the results have different portability and runtime tradeoffs compared to Rust-native binaries.
Native Extensions
Rust developers writing Python extensions will find PyO3 and maturin: PyO3 provides Rust bindings for the Python C API, and maturin is a build backend that compiles Rust into Python extension modules so uv build works with Rust extensions out of the box. The handbook’s Build a Python library with a Rust extension tutorial walks through the setup. This matters for performance-critical libraries but is not the typical Python workflow.
What Python Offers, and What Rust Developers Will Miss
Python brings capabilities that Rust’s ecosystem lacks. REPL-driven development through IPython allows testing ideas interactively without writing a file, and Jupyter notebooks combine code, prose, and visualizations in a single document, which is why Python dominates data science and machine learning. Tools like tox and nox automate testing across multiple Python versions, useful for library authors who need to support a range of interpreters. See Do you still need tox or nox if you use uv? for guidance on integrating these tools into a uv workflow.
Rust developers will miss the compiler as a safety net: ownership, lifetimes, exhaustive pattern matching, and guaranteed memory safety have no Python equivalent. Cargo’s unified, opinionated workflow is another loss; even with uv covering most of the surface, Python’s ecosystem reflects decades of independent tools built against shared standards rather than one design. Compile-time guarantees are traded for the speed and flexibility of an interpreted language.