Python for JavaScript Developers
The Python you need if you already think in JavaScript: collections, comprehensions, functions and keyword args, None vs null, classes and dataclasses, type hints, modules, and virtualenvs — taught as a delta from JS, with every idiom mapped to its JS equivalent.
- Map Python's core collections (list, dict, tuple, set) onto the JS types you already know.
- Replace
.map/.filter/.reducewith comprehensions, and template literals with f-strings. - Use functions with default and keyword arguments, and handle
Nonethe way you'd handlenull. - Write a typed
@dataclassand read type hints as you'd read TypeScript annotations. - Organise code into modules and run it inside a virtual environment.
You already know how to program. This topic teaches the delta from JavaScript, not loops-and-ifs from scratch. Every code block is commented, and Python-vs-JS differences are called out inline.
The shape of the language
Three things feel different coming from JS, and they explain most of the rest:
- Indentation is the block. No
{ }. A:opens a block and the indentation (4 spaces) is the body. No semicolons; a newline ends a statement. snake_case, notcamelCasefor variables and functions (classes arePascalCase). This is a strong convention, not just style.- Batteries included, one obvious way. The standard library is large, and the culture prizes a single "Pythonic" way to do things over clever one-liners.
def greet(name: str) -> str: # `:` opens the function body; `-> str` is the return type (a hint)
if name: # truthiness works like JS: empty string is falsy
return f"Hello, {name}" # f-string = JS template literal `Hello, ${name}`
return "Hello, stranger" # no semicolons, newline ends the statement
print(greet("Pierre")) # print() is console.log
Collections: list, dict, tuple, set
| Python | JS equivalent | Literal | Notes |
|---|---|---|---|
list | Array | [1, 2, 3] | ordered, mutable |
dict | Map / plain object | {"a": 1} | key→value; keys are usually strings |
tuple | frozen Array | (1, 2) | ordered, immutable — great for fixed pairs |
set | Set | {1, 2, 3} | unique items, no order |
nums = [1, 2, 3] # list ~ Array
nums.append(4) # .push(4)
nums[0] # 1 (indexing like JS)
nums[-1] # 4 (negative index = from the end; JS has no direct equivalent)
nums[1:3] # [2, 3] (slice: from index 1 up to—not including—3)
user = {"name": "Pierre", "role": "founder"} # dict ~ object/Map
user["name"] # "Pierre" (bracket access; NOT user.name)
user.get("age") # None (.get returns None if missing — no KeyError)
"name" in user # True (`in` checks keys, like `'name' in user` in JS)
point = (48.85, 2.35) # tuple — a fixed, immutable pair (lat, lng)
lat, lng = point # destructuring, like `const [lat, lng] = point`
tags = {"ai", "python", "ai"} # set literal; duplicates collapse
len(tags) # 2 (len() = .length / .size)
dict access uses brackets (user["name"]), not dots. user.name looks for an attribute and
will error. Use .get(key) when a key might be missing — it returns None instead of throwing.
↳ Exercise 1 has you translate an object/array manipulation from JS into the right Python
collections — the goal is to reach for dict/set reflexively instead of forcing everything into lists.
Comprehensions replace map / filter
This is the biggest day-to-day shift. Instead of chaining .map/.filter, Python builds a new list
inline with a comprehension.
words = ["rag", "agent", "eval", "llm"]
# JS: words.map(w => w.toUpperCase())
upper = [w.upper() for w in words] # ['RAG', 'AGENT', 'EVAL', 'LLM']
# JS: words.filter(w => w.length > 3)
longish = [w for w in words if len(w) > 3] # ['agent', 'eval']
# JS: words.filter(...).map(...) — combine filter + transform in one pass
result = [w.upper() for w in words if len(w) > 3] # ['AGENT', 'EVAL']
# Dict comprehension — build a dict (JS: Object.fromEntries(words.map(w => [w, w.length])))
lengths = {w: len(w) for w in words} # {'rag': 3, 'agent': 5, 'eval': 4, 'llm': 3}
Read [f(x) for x in items if cond] as "f(x), for each x in items, where cond" — it's
items.filter(cond).map(f) collapsed into one expression.
↳ Exercise 2 gives you three .map/.filter/.reduce chains to rewrite as comprehensions
(the reduce one uses sum(...), Python's built-in for the common case).
Functions, defaults, and keyword arguments
Python functions have a feature JS lacks: named (keyword) arguments you can pass by name, plus real default values.
def call_llm(prompt: str, model: str = "gpt-4o-mini", temperature: float = 0.7) -> str:
# ^ default value ^ default value — like JS default params
...
# Positional (order matters):
call_llm("Summarise this")
# Keyword — pass by name, any order; hugely common in Python APIs:
call_llm("Summarise this", temperature=0.2, model="gpt-4o")
# *args / **kwargs collect extra positional / keyword arguments:
def log(*args, **kwargs): # *args ~ JS rest (...args); **kwargs = a dict of named extras
print(args) # a tuple of the positional args
print(kwargs) # a dict of the keyword args
log("a", "b", level="warn") # ('a', 'b') then {'level': 'warn'}
↳ Exercise 3 (part A) has you write a function with a required arg, two keyword defaults, and call it three different ways — keyword arguments are everywhere in the AI SDKs you'll use next.
None, truthiness, and equality
result = None # None ~ null (there is no separate `undefined`)
if result is None: # use `is None`, not `== None` (identity check, the idiomatic way)
...
# Truthiness matches your intuition: 0, "", [], {}, None are falsy; everything else truthy.
items = []
if not items: # `not` ~ JS `!` — true when the list is empty
print("empty")
x = value or "default" # same short-circuit as JS: fall back when value is falsy
Python has no undefined. A missing dict key raises KeyError on d[key] (use d.get(key) → None),
and an unset variable is a NameError, not a silent undefined.
Classes and dataclasses
You'll write few raw classes for AI apps, but you'll read many. For plain data holders, @dataclass
gives you a typed record with almost no boilerplate — the closest thing to a TS interface + constructor.
from dataclasses import dataclass
@dataclass # a decorator: auto-generates __init__, __repr__, equality
class Chunk:
text: str # typed fields, like a TS interface
source: str
score: float = 0.0 # with a default
c = Chunk(text="hello", source="doc1.md") # constructor takes the fields by name
c.text # "hello" (attribute access with a dot — unlike dicts)
c.score = 0.9 # mutable by default
A regular class, for contrast — note self (Python's explicit this) as the first parameter of
every method:
class Retriever:
def __init__(self, k: int = 4): # __init__ is the constructor
self.k = k # `self` ~ `this`, but you must name it explicitly
def top_k(self, scores: list[float]) -> list[float]:
return sorted(scores, reverse=True)[: self.k] # `self.k` reads the instance field
Type hints
Type hints are optional annotations — like TypeScript types, but not enforced at runtime (a
separate tool, mypy, checks them). They make code and editor help far better; use them.
from typing import Optional
def find(id: str) -> Optional[Chunk]: # returns a Chunk OR None — like TS `Chunk | null`
...
names: list[str] = [] # annotate a variable, like `const names: string[] = []`
config: dict[str, int] = {"k": 4} # dict[KeyType, ValueType]
Modules and imports
Every .py file is a module. Imports are explicit and there's no bundler.
# file: rag/retriever.py
def retrieve(q: str): ...
# file: app.py (same package)
from rag.retriever import retrieve # named import
import rag.retriever as r # import the module, then r.retrieve(...)
if __name__ == "__main__": # the "am I being run directly?" guard
main() # runs only when you `python app.py`, not when app.py is imported
Virtual environments & packaging
Never install packages globally. A virtual environment is a project-local sandbox (like a
node_modules that also isolates the interpreter). Dependencies are listed in requirements.txt or
pyproject.toml (the package.json analog).
python -m venv .venv # create a virtual env in ./.venv (like an isolated node install)
source .venv/bin/activate # activate it (Windows: .venv\Scripts\activate)
pip install openai fastapi # install into THIS env only (pip ~ npm install)
pip freeze > requirements.txt # record exact versions (~ package-lock.json)
Modern teams increasingly use uv (a fast installer/manager): uv venv, uv pip install ...,
or uv add ... with a pyproject.toml.
↳ Exercise 4 walks you through creating a venv, installing a package, and importing it from your own module — the muscle memory you'll repeat at the start of every Python project.
Pitfalls coming from JS
d.keyvsd["key"]— dicts use brackets; dots are for object attributes.- Mutable default arguments — never write
def f(items=[]); the list is shared across calls. Usedef f(items=None): items = items or []. - Integer vs float division —
5 / 2 == 2.5(float),5 // 2 == 2(floor). JS only has one/. isvs==—==compares values;iscompares identity. Useisonly forNone/singletons.- No
++— usei += 1. - Truthy empties —
[],{},"",0,Noneare all falsy; checkif not items:for emptiness.
Recap
- Python's list/dict/tuple/set map to Array/Map+object/frozen-array/Set; dicts use bracket access.
- Comprehensions replace
.map/.filter; f-strings replace template literals. - Functions have keyword arguments and defaults — you'll use them constantly with AI SDKs.
Noneis yournull; useis Noneand.get()to stay safe.@dataclassis your typed record; type hints are optional TS-style annotations checked bymypy.- Isolate every project in a virtual environment; list deps in
requirements.txt/pyproject.toml.
Python for JavaScript Developers — Exercises
Do these in a scratch .py file inside a virtual environment (Exercise 4 sets that up — feel free to
do it first). Each exercise builds on a worked example from the course.
Exercise 1 — Reach for the right collection
You have this JS. Rewrite it in idiomatic Python using a dict and a set (not just lists).
const users = [
{ name: "Marie", tags: ["ai", "python"] },
{ name: "Tom", tags: ["js", "ai"] },
];
// 1. Build a name -> tags lookup
// 2. Get the set of all unique tags across users
Reference solution
users = [
{"name": "Marie", "tags": ["ai", "python"]}, # a list of dicts (array of objects)
{"name": "Tom", "tags": ["js", "ai"]},
]
# 1. name -> tags lookup (dict comprehension)
by_name = {u["name"]: u["tags"] for u in users} # {'Marie': [...], 'Tom': [...]}
# 2. union of all tags as a set (set comprehension flattening both lists)
all_tags = {tag for u in users for tag in u["tags"]} # {'ai', 'python', 'js'} — 'ai' appears once
The nested for u ... for tag ... reads left-to-right: outer loop then inner loop. The set
deduplicates "ai" automatically — that's why a set beats a list here.
Exercise 2 — Comprehensions instead of map/filter/reduce
Rewrite each JS chain as a Python comprehension (or built-in).
const nums = [1, 2, 3, 4, 5, 6];
const a = nums.map(n => n * n); // squares
const b = nums.filter(n => n % 2 === 0); // evens
const c = nums.reduce((s, n) => s + n, 0); // sum
Reference solution
nums = [1, 2, 3, 4, 5, 6]
a = [n * n for n in nums] # [1, 4, 9, 16, 25, 36]
b = [n for n in nums if n % 2 == 0] # [2, 4, 6] (note: `==`, and `%` like JS)
c = sum(nums) # 21 — Python has sum() built in; no manual reduce needed
For non-sum reductions you'd use functools.reduce, but reach for a built-in (sum, max, min,
any, all) first — there's usually one.
Exercise 3 — Keyword args + a typed dataclass
Part A. Write retrieve(query, k=4, rerank=False) and call it three ways: positional-only, with
one keyword, and with both keywords in a different order. (Just print the arguments; no real logic.)
Part B. Define a @dataclass called SearchResult with fields text: str, score: float, and
source: str = "unknown". Create one and print its score.
Reference solution
from dataclasses import dataclass
# Part A
def retrieve(query: str, k: int = 4, rerank: bool = False):
print(query, k, rerank)
retrieve("agents") # agents 4 False (defaults)
retrieve("agents", k=8) # agents 8 False (one keyword)
retrieve("agents", rerank=True, k=2) # agents 2 True (keywords, any order)
# Part B
@dataclass
class SearchResult:
text: str
score: float
source: str = "unknown" # default value
r = SearchResult(text="hello", score=0.87)
print(r.score) # 0.87 (attribute access with a dot)
print(r) # SearchResult(text='hello', score=0.87, source='unknown')
The auto-generated __repr__ (the last print) is one reason dataclasses are so handy for debugging.
Exercise 4 — Virtual env + install + import
From an empty folder: create a venv, activate it, install requests, then write two files — a module
tools.py with a function, and main.py that imports and calls it — and run main.py.
Reference solution
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests
pip freeze > requirements.txt # record the dependency
# file: tools.py
def add(a: int, b: int) -> int:
return a + b
# file: main.py
from tools import add # import your own module (same folder)
if __name__ == "__main__": # only runs when executed directly
print(add(2, 3)) # 5
python main.py # 5
Confirm the isolation: pip list inside the activated env shows requests; deactivate (deactivate)
and it's gone from your global Python. That sandboxing is the whole point.