Python Tooling for Node.js Developers
npm installs packages, locks versions, runs scripts, and publishes modules. Python splits those responsibilities differently, but uv consolidates most of them into a single command. This guide maps Node.js concepts to their Python equivalents, with emphasis on npm scripts and the daily runtime workflow. For tsc-style type-checking comparisons, see the companion TypeScript developers guide.
What Will Feel Familiar, and What Will Not
Most Node.js workflows have a recognizable Python counterpart:
| Node.js | Python | Notes |
|---|---|---|
| nvm / fnm / Volta | uv python install + uv python pin |
Python version management |
| npm / pnpm / yarn | uv | Dependency management |
package.json |
pyproject.toml | Project metadata and dependencies |
package-lock.json / pnpm-lock.yaml |
uv.lock |
Pinned dependency graph |
node_modules |
.venv (virtual environment) |
Per-project isolation |
npx |
uvx |
Run a CLI tool without permanent installation |
| ESLint | Ruff (linting) | |
| Prettier | ruff format |
|
| Jest / Mocha / Vitest | pytest | |
npm run <script> |
No direct equivalent | See Replacing npm scripts |
npm publish |
uv build + uv publish |
Build step is separate |
| nodemon | Framework-specific reloaders | No universal --watch |
Important
These analogies are orientation aids, not exact equivalences. Each pairing hides real differences in scope and behavior.
Two mental model gaps affect daily work.
Virtual environments are not node_modules. A node_modules folder 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 one is normal. Multiple environments can coexist on one machine for the same project, each pinned to a different Python version.
There is no built-in task runner. package.json scripts serve as the task runner for Node.js projects: npm run dev, npm test, npm run build. Python’s pyproject.toml has no equivalent section. Teams use uv run with explicit commands or Makefiles. This is the gap Node.js developers notice first.
Python’s Three-Layer Model
Node.js collapses several concerns into one tool: npm manages dependencies and runs scripts while using whatever Node version is on PATH. Python separates these into three layers.
Layer 1: The interpreter. uv python install 3.12 installs a Python runtime, similar to nvm install 20 or fnm install 20. A .python-version file pins the project’s Python version, serving the same role as .nvmrc or .node-version.
Layer 2: The dependency sandbox. A virtual environment is an isolated directory containing a Python interpreter (or symlink to one) and its own set of installed packages. It is attached to an existing interpreter; it does not install one. When you first run uv sync or uv run, uv creates and manages a .venv directory automatically, so the virtual environment is invisible for most workflows.
Layer 3: The project definition. pyproject.toml declares dependencies and project metadata. Tool configuration lives in [tool.*] sections (e.g., [tool.ruff], [tool.pytest.ini_options]) rather than in separate dotfiles, consolidating what Node.js spreads across .eslintrc and .prettierrc.
This three-layer separation explains why uv feels like nvm, npm, and npx merged into one tool: it bridges all three layers, installing Python versions, creating virtual environments, resolving dependencies, and running commands.
The Daily Development Loop
Adding and Managing Dependencies
Adding a dependency mirrors the npm workflow:
# Python (uv) # Node.js (npm)
uv add requests # npm install axios
uv add --dev pytest # npm install --save-dev jestuv add updates pyproject.toml and writes a lock file (uv.lock). To install from an existing lock file on a fresh clone:
uv sync # like npm ciuv sync is closer to npm ci than npm install: it installs exactly what the lock file specifies without modifying it. Running uv add is the equivalent of npm install <package>, which both installs and updates package.json.
Python supports optional extras and dependency groups for separating dev and test dependencies. These serve the same purpose as splitting dependencies from devDependencies in package.json, with more flexibility for additional groups.
Running Python Code
Running a script through uv:
uv run python main.py
uv run main.py # also works for .py filesuv run ensures the virtual environment exists and dependencies are installed before executing. It fills the role that node execution and node_modules resolution handle together.
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. Node.js developers familiar with the CommonJS require() and ESM import split will recognize the shape of this problem: how you invoke code affects what it can find. When in doubt, prefer uv run python -m mypackage.
Console scripts (entry points defined in pyproject.toml) serve a similar role to the "bin" field in package.json. 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.
Replacing npm Scripts
This is the gap Node.js developers feel most. A typical package.json might have:
{
"scripts": {
"dev": "nodemon server.js",
"test": "jest --coverage",
"lint": "eslint . && prettier --check .",
"start": "node server.js"
}
}Python’s pyproject.toml has no equivalent section. The practical default is to run commands directly with uv run and add a Makefile only when you want short aliases:
uv runwith explicit commands.uv run pytest --covreplacesnpm test. No aliases, but tab completion and shell history reduce the friction.- Makefile. A
Makefilewith targets likemake testandmake lintis the closest analog tonpm run. It works on macOS and Linux out of the box. Windows users needmakeinstalled or can use just as a cross-platform alternative. - Third-party tools. taskipy adds a
[tool.taskipy.tasks]section topyproject.toml, which is the closest structural match topackage.jsonscripts.
Start with direct uv run commands like uv run pytest and uv run ruff check .; add a Makefile only when the command list feels repetitive.
Code Quality: Linting and Formatting
Ruff handles both linting and formatting. It replaces the combination of ESLint and Prettier with one command:
uv run ruff check . # lint (like npx eslint .)
uv run ruff format . # format (like npx prettier --write .)Configuration lives in pyproject.toml under [tool.ruff], not in separate config files:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"] # enable specific rule setsWhere Node.js projects juggle .eslintrc, .prettierrc, and sometimes conflicts between the two, Ruff handles both concerns with zero configuration overlap.
Type Checking
Node.js developers who write plain JavaScript can skip this section and return when they want to add type annotations. Python’s type system is opt-in: code runs whether or not it has annotations.
For TypeScript developers, see the TypeScript guide’s type-checking section for a comparison with tsc.
mypy and pyright are the established type checkers. ty is a newer option from the team behind Ruff. See how they compare for help choosing.
uv run mypy .Because typing is gradual, most teams adopt it incrementally: add annotations to new code and tighten strictness over time. Legacy code often stays untyped until it needs changes.
Testing with pytest
pytest is Python’s standard testing framework. Its design differs from Jest and Mocha in a few ways:
- Tests are plain functions prefixed with
test_, not wrapped indescribe/itblocks. - Test discovery is by naming convention: files named
test_*.py(and*_test.py) containing functions namedtest_*. - Fixtures replace
beforeEach/afterEachwith a dependency-injection model.
# test_math.py
def test_addition():
assert 1 + 1 == 2No expect() or assertion library needed. Python’s built-in assert statement works directly, and pytest rewrites it at import time to produce detailed failure messages.
uv run pytest # like npx jestPackaging and Distribution
Library Packaging
Publishing to PyPI (the Python Package Index) is analogous to npm publish. The package format differs: Python libraries are distributed as wheels (prebuilt) and sdists (source distributions), built by a build backend.
uv build # creates wheel and sdist
uv publish # uploads to PyPInpm publishes whatever is in your project directory (minus .npmignore entries). Python’s separate build step exists because packages can contain compiled extensions (C, Fortran, Rust) that need platform-specific handling. For pure-Python packages, the build step adds a few seconds at most.
Application Deployment
Node.js applications deploy as source code with node_modules installed on the target. Python applications deploy the same way, with a virtual environment instead of node_modules.
The most common patterns:
- Container-based: a
Dockerfilethat installs dependencies withuv sync --frozenand copies the source. - Platform-managed: services like Heroku, Railway, or Render that read
pyproject.tomland install dependencies at deploy time.
Node.js developers using pm2 or forever for process management will find that gunicorn and uvicorn serve the same role for Python web applications: process supervision and worker management.
There is no bundling step. Python’s import system loads modules at runtime from the filesystem, so the concept of webpack or esbuild does not apply to server-side Python.
CLI Tools
npx runs a package’s binary without installing it globally. uvx does the same:
uvx ruff check . # like npx eslint .For permanent installation, uv tool install creates an isolated environment per tool, avoiding the dependency conflicts that npm install -g can cause:
uv tool install ruff # like npm install -g eslintWhat Python Offers, and What Node.js Developers Will Miss
Python’s REPL (and IPython) supports interactive development in ways that Node’s REPL cannot match for real workflows. Jupyter notebooks extend this into a development paradigm used across data science and machine learning. Tools like tox and nox automate testing across multiple Python versions, something the Node.js ecosystem has no equivalent for.
Node.js developers will miss package.json scripts most. The built-in task runner that comes free with every npm project has no Python counterpart, and third-party alternatives require choosing and configuring an extra tool. The --watch flag that ships with Node 18+ and tools like nodemon has no universal Python equivalent: web frameworks like Django and FastAPI provide their own reload mechanisms, but general-purpose file watching requires extra setup. And while uv workspaces exist, monorepo tooling comparable to Turborepo or Nx has not yet matured in the Python ecosystem.