# Python for JavaScript Engineers — A Practical Mental Model

Picking up Python as a JavaScript engineer is mostly straightforward because the mental models transfer well. Functions, modules, async patterns, data structures — the concepts are the same. What trips you up are the specific surface-level differences that look minor but cause real bugs: mutable default arguments, how imports work, when to use `dict` vs `TypedDict` vs `dataclass`.

This post is the reference I wish I'd had at the start — framed around the JS/TS concepts you already know.

## Setting Up a Project: The Node.js Mapping

The setup workflow maps almost 1:1. The key difference is that Python packages install globally by default, which is why virtual environments exist — they're Python's equivalent of `node_modules`.

| Node.js | Python |
| --- | --- |
| `package.json` | `requirements.txt` |
| `npm install` | `pip install -r requirements.txt` |
| `node_modules/` | `.venv/` (virtual environment) |
| `node index.js` | `python main.py` |
| `.env` | `.env` (same) |
| `nvm` | `pyenv` |

Creating and activating a virtual environment:

```bash
python -m venv .venv
source .venv/bin/activate   # macOS/Linux
```

**Important:** Unlike Node, you have to activate the virtual environment every time you open a new terminal session. Forgetting this is the most common setup error.

## Imports: The `__init__.py` Requirement

JavaScript imports work based on file paths. Python imports work based on module paths — and every directory that contains code you want to import needs an `__init__.py` file. This tells Python the directory is a module.

```plaintext
project/
  main.py
  src/
    __init__.py          ← required
    generator/
      __init__.py        ← required
      problem.py
```

```python
# Python equivalent of: import { generateProblem } from './src/generator/problem'
from src.generator.problem import generate_problem
```

Omitting `__init__.py` in any directory in the import chain causes a `ModuleNotFoundError` that's easy to mistake for a path problem.

## Type System: `dict` vs `TypedDict` vs `dataclass`

This is the decision that matters most when you start building anything non-trivial. The JavaScript equivalent is choosing between a plain object, an interface, and a class — but Python's options have different tradeoffs.

|  | `dict` | `TypedDict` | `class` / `dataclass` |
| --- | --- | --- | --- |
| Access syntax | `obj["key"]` | `obj["key"]` | `obj.attribute` |
| Type safety | None | Static (dev time) | Static + runtime potential |
| Can have methods | No | No | Yes |
| Performance | Best | Best (it's still a dict) | Slight overhead |
| JSON conversion | Native | Native | Requires serialization |

**Use** `TypedDict` for API responses and data shapes that flow between functions — it gives you editor autocomplete and static type checking with no runtime overhead:

```python
from typing import TypedDict, NotRequired, Literal

class Criterion(TypedDict):
    label: str
    points: int
    description: str
    evaluation_type: Literal["independent", "cascading"]
    evaluation_dependency: NotRequired[str]  # equivalent to key?: string in TS
```

**Use** `dataclass` for core application objects that need methods or validation.

**Use** `dict` for small, temporary data structures or when keys are generated at runtime.

## The Mutable Default Argument Bug

This one will bite you silently and is probably the most important Python gotcha for JS engineers.

```python
# WRONG — the list is shared across ALL calls to this function
def generate_problem(topic: str, past_problems: list = []):
    past_problems.append(topic)  # modifies the shared list

# CORRECT — use None, create a fresh list inside the function
def generate_problem(topic: str, past_problems: list = None):
    past_problems = past_problems or []
```

In JavaScript, default arguments are re-evaluated on every call. In Python, mutable defaults (lists, dicts) are created once when the function is defined and shared across all calls. The bug manifests as state accumulating unexpectedly across calls — hard to catch in testing, easy to miss in code review.

## Spreading, Ternary, and f-strings

Three syntax patterns that have direct JS equivalents:

**Spread operator:**

```python
# JavaScript: [...BASE_RUBRIC, ...EXERCISE_RUBRICS[type]]
# Python:
return [*BASE_RUBRIC, *EXERCISE_RUBRICS[exercise_type]]
```

**Ternary:**

```python
# JavaScript: condition ? value_when_true : value_when_false
# Python:
value_when_true if condition else value_when_false
```

**String interpolation (f-strings):**

```python
prompt = f"Generate a {difficulty} problem on the topic of {topic}."

# If your string contains literal curly braces, double them:
prompt = f"Return JSON in this format: {{\"key\": \"value\"}}"
```

## Environment Variables

Unlike frontend frameworks that handle `.env` files automatically, Node.js and Python both require explicit loading. The Python equivalent of dotenv:

```bash
pip install python-dotenv
```

```python
from dotenv import load_dotenv
import os

load_dotenv()
api_key = os.getenv("ANTHROPIC_API_KEY")
```

This trips up JS engineers who've been working primarily in Next.js or Vite, where env loading is handled by the framework. In a pure Python or pure Node script, you manage it yourself.
