Python Tooling for TypeScript Developers
TypeScript developers moving to Python often expect a single tool like npm that handles everything. Python splits those responsibilities differently, and the reasons behind that split affect how every tool behaves. This guide maps TypeScript/JS concepts to their Python equivalents and covers the daily workflow.
What Will Feel Familiar, and What Will Not
Most TypeScript/JS tools have a reasonable Python counterpart. The table below provides a rough translation.
| TypeScript / JavaScript | Python |
|---|---|
| nvm / fnm | uv Python version management (uv python install + uv python pin) |
| npm / pnpm / yarn | uv (package management) |
package.json |
pyproject.toml |
package-lock.json / pnpm-lock.yaml |
uv.lock |
node_modules |
.venv (project-local isolated environment containing an interpreter and site-packages) |
| ESLint | ruff (linting) |
| Prettier | ruff format |
tsc --noEmit (type checking) |
mypy / pyright / ty |
| Jest / Vitest | pytest |
tsconfig.json |
[tool.*] sections in pyproject.toml (or dedicated config files like ruff.toml) |
npx |
uvx |
| npm workspaces | uv workspaces |
Important
These analogies are orientation aids, not exact equivalences. Each Python tool has its own design philosophy and behavior that differs from the JS tool it loosely maps to.
Two mental model gaps trip up TypeScript developers more than anything else.
Virtual environments are not node_modules. A node_modules folder is just a directory of installed packages. A Python virtual environment is an isolated copy (or symlink) of a Python interpreter plus its own package directory. Deleting and recreating one is normal, and multiple environments can coexist on one machine for the same project.
Python separates standards from implementations. In the JS world, npm defines both the registry protocol and the dominant tool. Python separates standards (defined by PEPs) from implementations, so multiple tools can implement the same standard in different ways. This is why there are so many Python packaging tools.
One other difference worth noting: Python has no universal hot-reload convention. Node has nodemon, tsx --watch, and built-in --watch flags across many tools. Some Python frameworks (Django, Flask, FastAPI) provide their own reload mechanisms, but there is no cross-framework standard. See how hot reloading works in Python for why, and how to set up auto-reload for practical setup instructions.
Python’s Three-Layer Model
Node.js collapses several concerns into one tool: npm manages dependencies, runs scripts, and implicitly uses whatever Node version is on PATH. Python separates these into three distinct layers.
Layer 1: The runtime. Python version management is independent of package management. uv python install 3.12 installs a Python interpreter, similar to how nvm install 18 installs a Node version. But unlike nvm, the version manager and the package manager are the same tool (uv), even though the operations are conceptually separate.
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 Python interpreter; it does not install one. When you first run uv sync or uv run, uv creates and manages a .venv directory automatically, so for most workflows the virtual environment is invisible.
Layer 3: The project definition. pyproject.toml is the central project configuration file, similar to package.json. It declares dependencies, project metadata, and tool-specific settings. It has a [project.scripts] section for defining installed console entry points, but no built-in task-runner equivalent to package.json scripts. Tool configuration lives in [tool.*] sections (e.g., [tool.ruff], [tool.pytest.ini_options]) rather than in separate dotfiles.
This separation means you can change your Python version without touching project metadata, or swap out your package manager without changing your project file. You can also run the same project against multiple Python versions for testing (though dependency resolution may differ per version, requiring a re-sync). The flexibility comes from the standards-based approach: PEPs define interfaces, and tools implement them.
The Daily Development Loop
Adding and Managing Dependencies
Adding a package works the way you’d expect:
# Python (uv) # TypeScript (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 lockfile on a fresh clone:
uv sync # like npm installRunning Python Code and Import Semantics
Running a script through uv:
uv run python main.py
uv run main.py # also works for .py filesPython has two ways to execute code that have no direct Node equivalent. python file.py runs a file directly. python -m package runs a package as a module, which matters for import resolution. In Node, node file.js handles both cases. In Python, the distinction affects whether relative imports work and how sys.path is constructed. When in doubt, prefer uv run python -m mypackage.
Python projects that expose command-line tools define console scripts (entry points) in pyproject.toml, analogous to the "bin" field in package.json. After installation, these are available as commands without needing python -m.
Many Python projects use a src/ layout where the package code lives in src/mypackage/ rather than at the project root. During development, the package is installed as an editable install so that code changes are reflected without reinstalling. uv sync handles this automatically when the project is configured as a package.
There is no universal --watch flag or nodemon equivalent. Web frameworks like Django and FastAPI include their own reload mechanisms, but for general Python scripts, automatic reload is not a standard feature.
Code Quality: Linting and Formatting
Ruff handles both linting and formatting in a single tool. It replaces the combination of ESLint and Prettier with one command:
uv run ruff check . # lint (like eslint .)
uv run ruff format . # format (like prettier --write .)Configuration lives in pyproject.toml under [tool.ruff], not in a separate config file:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"] # enable specific rule setsType Checking
TypeScript developers are used to types being inseparable from the language. Python’s type system is opt-in. Code runs whether or not it has type annotations, and type checking is a separate step performed by an external tool.
Three type checkers are in active use: mypy, pyright, and ty. There is no single tsc equivalent. Each checker interprets Python’s typing spec with slightly different strictness and speed tradeoffs. See how mypy, pyright, and ty compare for help choosing.
uv run mypy . # like tsc --noEmitBecause 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
pytest is the dominant testing framework. Its design differs from Jest/Vitest 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_*. pytest also discoversTest*classes and supportsunittest.TestCasetests. - Fixtures replace
beforeEach/afterEachwith a dependency-injection model.
# test_math.py
def test_addition():
assert 1 + 1 == 2uv run pytest # like npx jestPackaging and Distribution
Library Packaging
Publishing a Python library to PyPI (the Python Package Index) is analogous to npm publish. The package format is different: Python libraries are distributed as wheels (prebuilt) and sdists (source distributions), built by a build backend like hatchling or setuptools.
uv build # creates wheel and sdist
uv publish # uploads to PyPIApplication Deployment
Most Python applications are never “built” in the way a TypeScript app is bundled by webpack or esbuild. There is no equivalent of a JS bundle for typical Python server applications.
Instead, Python apps are deployed as source code plus a locked environment. The most common patterns are:
- Container-based: A
Dockerfilethat installs dependencies withuv sync --frozenand copies the source. - Platform-managed: Services like Heroku or Railway that read
pyproject.tomland install dependencies at deploy time.
The absence of a bundling step can feel strange coming from the JS world. Python’s import system loads modules at runtime from the filesystem, so there is nothing to resolve or tree-shake ahead of time for server applications.
What You Gain and What You Lose
Python’s REPL (and IPython) lets you test code interactively in ways that the node REPL rarely supports for real workflows. Jupyter notebooks extend this into an entire development and documentation paradigm, especially in data science. Tools like tox and nox also let you run test suites against multiple Python versions in a single command, something the JS ecosystem has no widely-adopted equivalent for.
On the other hand, TypeScript developers will miss the convenience of package.json scripts as a built-in task runner. Python has no equivalent; teams use Makefiles, taskipy, or just document the commands in a README. The --watch flag that is everywhere in the Node world is fragmented across frameworks in Python. And while uv workspaces exist, monorepo tooling comparable to Turborepo or Nx has not yet matured in the Python ecosystem.