Skip to main content

Command Palette

Search for a command to run...

Python for JavaScript Engineers — A Practical Mental Model

Updated
4 min read
D
Senior engineer with deep roots in React, TypeScript, and production-scale UI — including 4 years at Apple. Now focused on applied AI engineering: working hands-on with LLM APIs, RAG pipelines, agents, and the full stack of tooling that modern AI products are built on. Writing here to document the build, share what's non-obvious, and connect with teams working on hard AI problems.

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.

4 views

Python for JS

Part 1 of 1