Skip to main content

Python for Node.js Developers

Python is the lingua franca of AI engineering. From PyTorch and TensorFlow to LangChain and LlamaIndex, every major framework in the AI ecosystem is built in Python. If you come from a Node.js and TypeScript background, you already have the mental models you need β€” you just need to learn the new vocabulary and tooling.

This guide bridges that gap. By the end, you'll understand how your existing knowledge maps to the Python world and be ready to contribute to our AI engineering projects.


Why Python for AI Engineering​

The AI/ML ecosystem is overwhelmingly Python-native. Here is why:

  • Framework dominance. PyTorch, TensorFlow, JAX, LangChain, LlamaIndex, Hugging Face Transformers β€” all first-class Python APIs.
  • Data science stack. NumPy, Pandas, Polars, scikit-learn form the foundation of data manipulation and analysis.
  • Rapid prototyping. Python's dynamic typing and REPL-friendly nature make it ideal for experimentation.
  • Community and documentation. When you encounter an error in an AI library, the top search results will almost always be Python-centric.

This does not mean Python replaces TypeScript in your toolkit. Many of our services use a hybrid approach: Python for AI/ML workloads, TypeScript for APIs and frontend. But you need to be fluent in both.


The Mental Model: Node.js β†’ Python Mapping​

The fastest way to learn Python is to understand what corresponds to the tools you already know. The following table maps your existing ecosystem to the Python equivalents we use on this team.

Node.js / JavaScript ConceptPython EquivalentNotes
npmuv pipuv is our package manager β€” orders of magnitude faster than pip
package.jsonpyproject.tomlSingle file for metadata, dependencies, and tool configuration
node_modules/.venv/Isolated virtual environment for project dependencies
tsconfig.json + TypeScriptType hints + tyPython has optional static typing; ty checks it at 10–100Γ— the speed of mypy
ESLint + PrettierruffOne tool replaces both linting and formatting, 10–100Γ— faster than Flake8/Black
JestpytestThe de facto Python test runner
npxuvxRun a tool without installing it globally
npm inituv initBootstrap a new project with sensible defaults
package-lock.jsonuv.lockDeterministic lockfile for reproducible installs
.eslintrc / .prettierrcpyproject.toml (inline)Tool config lives alongside project metadata
ts-nodeuv runExecute Python scripts with dependencies resolved
Key Insight

The biggest conceptual shift is not syntax β€” it is dependency management. In Node.js, npm install writes to node_modules/. In Python, uv sync writes to .venv/. The virtual environment is your project's isolated runtime, and uv manages it automatically.


Setting Up Your Environment with uv​

What is uv?​

uv is an extremely fast Python package and project manager written in Rust by Astral. It replaces pip, virtualenv, pip-tools, and more β€” all while being orders of magnitude faster. It is the single most important tool in the Python workflow for our team.

Installing uv​

curl -LsSf https://astral.sh/uv/install.sh | sh

Verify the installation:

$ uv --version
uv 0.x.x
Shell Autocompletion

Enable autocompletion for a better experience:

# zsh (default on macOS)
echo 'eval "$(uv generate-shell-completion zsh)"' >> ~/.zshrc

# bash
echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc

Your First Project​

Here is the complete workflow side by side with its npm equivalent:

# 1. Create project directory
mkdir my-project && cd my-project

# 2. Initialize package.json
npm init -y

# 3. Install dependencies
npm install express
npm install --save-dev typescript eslint prettier

# 4. Install and lock
npm ci # uses package-lock.json

# 5. Run a script
npm run dev

Understanding the Project Files​

After running uv init, your project looks like this:

my_project/
β”œβ”€β”€ .git/
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .python-version # Pinned Python version (e.g., "3.12")
β”œβ”€β”€ .venv/ # Virtual environment (created on first uv sync/run)
β”‚ β”œβ”€β”€ bin/
β”‚ β”œβ”€β”€ lib/
β”‚ └── pyvenv.cfg
β”œβ”€β”€ main.py # Entry point
β”œβ”€β”€ pyproject.toml # Project metadata + dependencies
└── uv.lock # Lockfile (like package-lock.json)

The pyproject.toml is the heart of your project configuration:

[project]
name = "my_project"
version = "0.1.0"
description = "My AI engineering project"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi",
"pydantic",
]

[tool.uv.dev-dependencies]
ruff = ">=0.4.0"
ty = ">=0.0.1"
pytest = ">=8.0.0"
npm β†’ uv Command Cheat Sheet
npm commanduv equivalent
npm install <pkg>uv add <pkg>
npm uninstall <pkg>uv remove <pkg>
npm install (from lockfile)uv sync
npm run <script>uv run <command>
npx <tool>uvx <tool>
npm outdateduv lock --upgrade-package <pkg>
npm lsuv tree

Python Syntax Crash Course​

This section highlights the key differences between JavaScript/TypeScript and Python. It is not a complete Python tutorial β€” for that, see the official Python tutorial. Instead, it focuses on what will trip you up coming from JavaScript.

Variables and Type Hints​

Python is dynamically typed, but you can (and should) add type hints. Think of type hints as TypeScript types written inline.

let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let tags: string[] = ["ai", "python"];
let config: Record<string, unknown> = { key: "value" };

// Optional types
let middleName: string | undefined;

Key differences:

  • No let/const/var. All variables are mutable by default. If you need an immutable constant, by convention, use UPPER_SNAKE_CASE.
  • null β†’ None. Python has a single null value: None.
  • true/false β†’ True/False. Note the capitalization.
  • undefined does not exist. Uninitialized variables raise a NameError.
  • Type hints are optional at runtime. They exist for static analysis (checked by ty), not for runtime enforcement.

Functions and Type Annotations​

function greet(name: string, times: number = 1): string {
return Array(times).fill(`Hello, ${name}!`).join("\n");
}

// Async function
async function fetchData(url: string): Promise<string> {
const response = await fetch(url);
return response.text();
}

Key differences:

  • function β†’ def. Python uses the def keyword.
  • Parameter syntax. Default values use = just like JavaScript, but the type annotation goes before the =.
  • Return types. The arrow -> specifies the return type, placed after the parameter list.
  • No Promise. Python uses async/await with coroutines. The async def syntax is similar, but there is no Promise type β€” the return type hint is the unwrapped value.
  • Indentation matters. Python uses whitespace (4 spaces) for block structure, not curly braces.

Classes and Dataclasses​

Python has classes, but for data-carrying structures, dataclasses are the Python equivalent of TypeScript interfaces or DTOs.

interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "moderator";
}

class UserService {
private users: User[] = [];

add(user: User): void {
this.users.push(user);
}

findById(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
}

Key differences:

  • interface β†’ @dataclass. Dataclasses auto-generate __init__, __repr__, and __eq__ methods from type annotations.
  • class fields. Python uses self explicitly as the first parameter of every instance method.
  • Private fields. Python uses a leading underscore _name as a convention for private (not enforced by the language).
  • | union syntax. Python 3.10+ supports X | Y for union types, just like TypeScript.
  • Literal type. For enumerated string values, use Literal["a", "b"] instead of a union of string literals.

List and Dict Comprehensions​

Python comprehensions are the equivalent of map/filter/reduce chains β€” but more concise.

// Filter and transform
const ids = users
.filter(u => u.role === "admin")
.map(u => u.id);

// Build an object from an array
const userMap = users.reduce((acc, u) => {
acc[u.id] = u.name;
return acc;
}, {} as Record<number, string>);
Comprehension Syntax

The pattern is always: [expression for item in iterable if condition]. Read it left to right: "for each item in iterable, if condition, collect expression."

f-Strings​

Python's f-strings are the equivalent of template literals, with a slightly different syntax.

const name = "Alice";
const score = 95;

console.log(`User ${name} scored ${score} points`);
console.log(`Result: ${score > 90 ? "pass" : "fail"}`);

Async/Await in Python​

Python's async model is similar to JavaScript's but has some important differences.

async function main() {
const [data1, data2] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
]);
console.log(data1, data2);
}

Key differences:

  • No global event loop. Python requires an explicit asyncio.run() to start the event loop (unlike Node.js which has one built in).
  • Promise.all β†’ asyncio.gather. The equivalent for running coroutines concurrently.
  • async with β†’ context managers. Python's async with is the equivalent of wrapping I/O in a context that handles cleanup (like try/finally).

Project Structure​

Python projects follow a different convention than Node.js projects. Understanding the layout is critical for knowing where to put code and how imports work.

my-project/
β”œβ”€β”€ package.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ .eslintrc.json
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ index.ts # Entry point
β”‚ β”œβ”€β”€ routes/
β”‚ β”‚ └── users.ts
β”‚ β”œβ”€β”€ services/
β”‚ β”‚ └── ai-service.ts
β”‚ └── utils/
β”‚ └── helpers.ts
β”œβ”€β”€ tests/
β”‚ └── users.test.ts
└── dist/ # Compiled output
__init__.py Files

Every directory that contains Python code needs an __init__.py file (which can be empty). This is Python's way of saying "this directory is a package." It is the equivalent of how Node.js treats any folder as importable by default.

Import Paths​

In Node.js, you import using file paths:

import { getUser } from "../services/ai-service";

In Python, you import using package names:

from my_project.services.ai_service import get_user

The import path starts from the package root (the directory containing __init__.py), not from the current file. This is one of the most common sources of confusion for Node.js developers.


Our Tooling Stack​

Our team uses three tools from the Astral ecosystem. Together, they replace what would be six or more separate tools in the JavaScript world.

ruff β€” Linting and Formatting​

ruff is an extremely fast Python linter and code formatter written in Rust. It replaces Flake8, isort, Black, pydocstyle, pyupgrade, autoflake, and dozens of plugins β€” all while running 10–100Γ— faster.

Key commands:

# Lint the project
uvx ruff check .

# Auto-fix fixable issues
uvx ruff check --fix .

# Format the project
uvx ruff format .

# Lint and format in one go
uvx ruff check --fix . && uvx ruff format .

Configuration lives in pyproject.toml:

[tool.ruff]
target-version = "py312"
line-length = 88

[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D100", # Missing docstring on module
"D104", # Missing docstring on public package
]

[tool.ruff.lint.pydocstyle]
convention = "google"
ruff vs ESLint + Prettier

In the JavaScript world, you configure ESLint for linting, Prettier for formatting, and often add plugins (eslint-plugin-import, eslint-plugin-unused-imports, etc.) to bridge gaps. With ruff, one tool does everything with over 900 built-in rules.

ty β€” Type Checking​

ty is an extremely fast Python type checker and language server written in Rust. It is 10–100Γ— faster than mypy and Pyright, making it practical to run on every edit.

Key commands:

# Type check the project
uvx ty check

# Type check a single file
uvx ty check src/my_project/main.py

Configuration in pyproject.toml:

[tool.ty]
python-version = "3.12"
ty is Currently in Beta

ty is production-ready for most use cases but is still in beta. Its type system supports Python 3.10+ and covers the vast majority of typing features including generics, protocols, and type narrowing.

Pre-commit Hooks​

We wire ruff and ty into pre-commit hooks so that quality checks run automatically before every commit. A typical .pre-commit-config.yaml:

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/astral-sh/ty
rev: v0.0.33
hooks:
- id: ty

Install the hooks:

uv add --dev pre-commit
uv run pre-commit install

After installation, ruff and ty run automatically on staged files whenever you run git commit.


Your First Day Checklist​

Here is a practical checklist to get you productive on day one:

  1. Install uv β€” curl -LsSf https://astral.sh/uv/install.sh | sh
  2. Clone the repository β€” git clone <repo-url> && cd <repo-name>
  3. Sync dependencies β€” uv sync
  4. Run the linter β€” uvx ruff check .
  5. Run the type checker β€” uvx ty check
  6. Run the tests β€” uv run pytest
  7. Install pre-commit hooks β€” uv run pre-commit install
  8. Make your first change β€” write code, commit, and let the hooks validate it

Key Takeaways​

Summary
  • uv is your new npm. It manages packages, virtual environments, and lockfiles β€” all in one extremely fast tool.
  • pyproject.toml is your new package.json. It holds project metadata, dependencies, and tool configuration in a single file.
  • Type hints + ty replace TypeScript. Python type hints give you static analysis without sacrificing dynamic flexibility.
  • ruff replaces ESLint + Prettier. One tool handles linting, formatting, and import sorting with 900+ rules.
  • .venv/ replaces node_modules/. Each project gets an isolated virtual environment.
  • uvx replaces npx. Run tools without global installation.
  • Comprehensions replace map/filter chains. Python's list and dict comprehensions are concise and expressive.
  • asyncio.run() starts the event loop. Unlike Node.js, Python requires an explicit entry point for async code.

Further Reading​