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:
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.
project/
main.py
src/
__init__.py ← required
generator/
__init__.py ← required
problem.py
# 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:
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.
# 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:
# JavaScript: [...BASE_RUBRIC, ...EXERCISE_RUBRICS[type]]
# Python:
return [*BASE_RUBRIC, *EXERCISE_RUBRICS[exercise_type]]
Ternary:
# JavaScript: condition ? value_when_true : value_when_false
# Python:
value_when_true if condition else value_when_false
String interpolation (f-strings):
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:
pip install python-dotenv
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.

