Skip to content

Python Tooling for PHP Developers

Composer, PHPUnit, PHP-CS-Fixer, PHPStan. PHP developers already work with a specialized toolchain. Python’s tooling is organized around the same concerns but splits them differently, and the differences in how each language handles isolation, versioning, and execution shape 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, lock files, 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/. Composer’s vendor/ directory holds downloaded packages for one project. A Python virtual environment goes further: it contains a Python interpreter (or symlink to one) and its own package directory, forming a self-contained execution context. Deleting and recreating a virtual environment is routine. You can also have multiple environments for the same project, each pinned to a different Python version.

Standards and tools are separate. PHP’s packaging ecosystem is defined by Composer. Python separates standards (defined by PEPs) from implementations, so multiple tools can implement the same interface. This is why so many Python packaging tools exist. uv covers most of the surface area, 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 all execution, Python version management is an explicit, separate operation. 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’s isolation is at the process level. When a virtual environment is active, the python command itself resolves to that environment’s interpreter, and only packages installed in that environment are importable. No autoloader needed.

Layer 3: The project definition. pyproject.toml declares metadata, dependencies, and tool configuration. It plays a role similar to composer.json, though it is lighter: no autoload configuration and no post-install scripts by default. Tool configuration for linters, formatters, and test runners lives in [tool.*] sections within 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 install

uv add updates pyproject.toml and writes a lock file (uv.lock). uv sync installs from the lock file, 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 files

uv 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. python -m package runs a package as a module, which adjusts how imports resolve. When a project uses relative imports, the distinction matters. In PHP, the 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 sets

Ruff 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 type hints) and has steadily expanded them through 8.x. Python’s type annotations arrived in 3.5 and have grown similarly. The key difference: PHP’s runtime enforces type declarations by default (a TypeError is thrown if an argument does not match). 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 over the codebase, get a report of type errors, fix them. The difference is that Python’s type checker is the only enforcement mechanism. There is no runtime fallback.

uv run mypy .                # like vendor/bin/phpstan analyse

Because 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 == 2

No 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/phpunit

Packaging and Distribution

Library Packaging

Composer packages are source distributions: the registry (Packagist) serves the source, and Composer downloads it. Python packages come in two forms: source distributions (sdists) and wheels (pre-built packages). When a wheel is available, installation skips the build step. A build backend handles creating both formats.

Publishing to PyPI is conceptually similar to publishing on Packagist:

uv build                     # creates wheel and sdist
uv publish                   # uploads to PyPI

Application 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 (“post-install-cmd”, “test”, custom scripts). Python has no equivalent in pyproject.toml; teams use Makefiles or uv run with specific commands. The autoloading system that makes classes available without explicit imports has no Python parallel; Python requires explicit import statements for every module. And while Python’s ecosystem covers web, data science, ML, automation, and scripting, PHP’s integration with web servers and the CMS/framework ecosystem (Laravel, WordPress) has no direct counterpart in Python’s web story.

Last updated on