Python Tooling for PHP Developers
Composer, PHPUnit, PHP-CS-Fixer, PHPStan: PHP developers already work with a specialized toolchain. Python’s tooling covers the same concerns but splits them differently, and how each language handles isolation, versioning, and execution shapes every tool in the stack. This guide maps PHP concepts to their Python equivalents and explains where the mental models diverge.
What Will Feel Familiar, and What Will Not
Most PHP workflows have a recognizable Python counterpart:
| PHP | Python | Notes |
|---|---|---|
| php-build / phpenv | uv python install |
Python version management |
| Composer | uv | Dependency management, lockfiles, script running |
composer.json |
pyproject.toml | Central project configuration |
composer.lock |
uv.lock |
Pinned dependency versions |
| Packagist | PyPI | Public package registry |
vendor/ |
.venv (virtual environment) |
Per-project dependency isolation |
| PHPUnit | pytest | Testing framework |
| PHP-CS-Fixer / PHP_CodeSniffer | Ruff | Linting and formatting |
| PHPStan / Psalm | mypy / pyright / ty | Static analysis and type checking |
composer global require |
uv tool install / uvx |
System-wide CLI tools |
Important
These analogies are orientation aids, not exact equivalences. Each pairing hides real differences in scope and behavior.
Two mental model gaps will affect daily work.
Virtual environments are not vendor/. Where Composer’s vendor/ holds downloaded packages for one project, a Python virtual environment also contains a Python interpreter (or symlink) and its own package directory, forming a self-contained execution context. Deleting and recreating one is routine, and a project can have several, each pinned to a different Python version.
Standards and tools are separate. PHP’s packaging ecosystem is defined by Composer, whereas Python separates standards (defined by PEPs) from implementations, so multiple tools can implement the same interface, which is why so many Python packaging tools exist. uv covers most of the surface, but the standards-vs-tools distinction explains choices that otherwise seem arbitrary.
Python’s Three-Layer Model
PHP collapses execution and dependency management into two pieces: a PHP binary and Composer. The PHP binary runs code directly, and Composer handles everything else (dependencies, autoloading, scripts). Python separates these concerns into three layers.
Layer 1: The interpreter. A specific version of CPython, managed with uv python install 3.12. Unlike PHP, where a single php binary handles execution, Python version management is an explicit, separate operation, and multiple interpreters can coexist on one machine.
Layer 2: The virtual environment. A virtual environment is a directory containing a Python executable (via symlink or copy) and its own site-packages folder for installed packages. It is attached to an existing interpreter but does not install one.
PHP relies on Composer’s vendor/autoload.php for per-project isolation: include the autoloader and the project’s dependencies become available. Python isolates at the process level: when a virtual environment is active, the python command resolves to that environment’s interpreter, and only packages installed there are importable. No autoloader needed.
Layer 3: The project definition. pyproject.toml declares metadata, dependencies, and tool configuration, playing a role like composer.json but lighter: no autoload configuration and no post-install scripts by default. Configuration for linters, formatters, and test runners lives in [tool.*] sections of the same file.
This three-layer separation explains a design difference PHP developers notice quickly: uv bridges all three layers. It installs Python versions, creates virtual environments, resolves dependencies, and runs scripts. For PHP developers, think of it as if phpenv and Composer merged into one tool.
The Daily Development Loop
Adding and Managing Dependencies
Composer and uv follow the same pattern: declare, lock, install.
# Python (uv) # PHP (Composer)
uv add requests # composer require guzzlehttp/guzzle
uv add --dev pytest # composer require --dev phpunit/phpunit
uv sync # composer installuv add updates pyproject.toml and writes a lockfile (uv.lock). uv sync installs from the lockfile, the same workflow as composer install reading composer.lock.
Python has no direct equivalent of Composer’s autoloading configuration (autoload and autoload-dev in composer.json). Python’s import system resolves modules by searching directories on sys.path, and virtual environments manage which directories are on that path. There is nothing to configure.
Python also supports optional extras (published package metadata that consumers can request at install time, like uv add "httpx[http2]") and dependency groups for organizing dev, test, and documentation dependencies separately.
Running Python Code
PHP runs files directly: php index.php. Python is similar:
uv run python main.py
uv run main.py # also works for .py filesuv run ensures the virtual environment is active and dependencies are installed before executing the command. It fills the role of both php execution and the implicit vendor/autoload.php include.
Python has two execution modes that affect import resolution: python file.py runs a single file, while python -m package runs a package as a module, which adjusts how imports resolve and matters when a project uses relative imports. Where PHP’s autoloader handles this transparently, in Python how you invoke the code affects what it can import.
Console scripts (entry points defined in pyproject.toml) serve a similar role to Composer’s bin declarations. They create named commands installed into the environment’s bin/ directory:
[project.scripts]
myapp = "mypackage.cli:main"For library development, the src/ layout keeps source code separate from configuration and tests. Combined with an editable install (which uv sync handles automatically), code changes take effect without reinstalling.
Code Quality: Linting and Formatting
PHP projects typically use PHP-CS-Fixer or PHP_CodeSniffer for formatting and code style, sometimes with separate tools for different concerns. Python consolidates linting and formatting into Ruff:
uv run ruff check . # lint (like php-cs-fixer fix --dry-run)
uv run ruff format . # format (like php-cs-fixer fix)Configuration lives in pyproject.toml under [tool.ruff], not in a separate file like .php-cs-fixer.php:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"] # enable specific rule setsRuff re-implements rules from dozens of older Python linting tools, so most projects need only this single dependency for code quality.
Type Checking
PHP and Python both adopted gradual typing after years without it: PHP added type declarations in 7.0 (scalar hints) and expanded them through 8.x, and Python’s annotations arrived in 3.5 and grew similarly. The key difference is enforcement: PHP’s runtime enforces type declarations by default (throwing a TypeError on a mismatch), while Python’s runtime ignores annotations entirely. Type checking requires a separate tool.
Three type checkers are in active use: mypy, pyright, and ty. PHP developers familiar with PHPStan or Psalm will recognize the workflow: run a static analyzer, get a report of type errors, fix them. The difference is that Python’s type checker is the only enforcement mechanism, with no runtime fallback.
uv run mypy . # like vendor/bin/phpstan analyseBecause typing is gradual, it is common to adopt it incrementally: add annotations to new code, tighten strictness over time, and leave legacy code untyped until it needs changes.
Testing with pytest
PHPUnit organizes tests in classes that extend TestCase, with methods prefixed by test. pytest takes a lighter approach: write plain functions whose names start with test_, and pytest discovers and runs them.
# test_math.py
def test_addition():
assert 1 + 1 == 2No class hierarchy. No $this->assertEquals(). Python’s built-in assert statement works directly, and pytest rewrites it at import time to produce detailed failure messages.
Where PHPUnit uses setUp() and tearDown() methods, pytest uses fixtures: functions decorated with @pytest.fixture that provide test dependencies through injection. Tests declare which fixtures they need as function parameters, and pytest wires them up automatically.
uv run pytest # like vendor/bin/phpunitPackaging and Distribution
Library Packaging
Composer packages are source distributions: Packagist serves the source and Composer downloads it. Python packages come as source distributions (sdists) and pre-built wheels; when a wheel is available, installation skips the build step. A build backend creates both formats.
Publishing to PyPI is conceptually similar to publishing on Packagist:
uv build # creates wheel and sdist
uv publish # uploads to PyPIApplication Deployment
PHP applications run behind a web server (Nginx + PHP-FPM, Apache + mod_php) and deploy as source code. Python web applications are similar in that they deploy as source code, but the execution model differs: Python web frameworks (Django, Flask, FastAPI) typically run behind an application server (gunicorn, uvicorn) that the project declares as a dependency, rather than relying on a separate system-level runtime like PHP-FPM.
Deployment usually means a container image that installs dependencies with uv sync --frozen and runs the application server. There is no compilation or bundling step for pure Python applications.
CLI Tools
Composer’s global require installs a package system-wide. Python’s equivalent is uv tool install, which creates an isolated environment per tool to avoid dependency conflicts between tools:
uv tool install ruff # like composer global require friendsofphp/php-cs-fixer
uvx ruff check . # run without permanent install (like composer exec)What Python Offers, and What PHP Developers Will Miss
Python’s REPL and IPython let developers test ideas interactively without writing a file, something PHP’s php -a interactive mode supports in principle but rarely matches in practice. Jupyter notebooks extend this into a full document format used for data science and exploratory work. Tools like tox and nox automate testing across multiple Python versions in a single command.
PHP developers will miss a few things. Composer’s scripts section provides a built-in task runner (test, post-install-cmd) that pyproject.toml has no equivalent for; teams use Makefiles or uv run with specific commands. Autoloading has no Python parallel (every module needs an explicit import), and PHP’s tight integration with web servers and the Laravel/WordPress ecosystem has no direct counterpart in Python’s web story.