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.

~3hpythontyping
Learning objectives
  • Map Python's core collections (list, dict, tuple, set) onto the JS types you already know.
  • Replace .map/.filter/.reduce with comprehensions, and template literals with f-strings.
  • Use functions with default and keyword arguments, and handle None the way you'd handle null.
  • Write a typed @dataclass and read type hints as you'd read TypeScript annotations.
  • Organise code into modules and run it inside a virtual environment.
Note

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:

  1. Indentation is the block. No { }. A : opens a block and the indentation (4 spaces) is the body. No semicolons; a newline ends a statement.
  2. snake_case, not camelCase for variables and functions (classes are PascalCase). This is a strong convention, not just style.
  3. 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

PythonJS equivalentLiteralNotes
listArray[1, 2, 3]ordered, mutable
dictMap / plain object{"a": 1}key→value; keys are usually strings
tuplefrozen Array(1, 2)ordered, immutable — great for fixed pairs
setSet{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)
Watch out

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.

Tip

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.

Tip

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'}
Tip

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
Watch out

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.

Tip

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.key vs d["key"] — dicts use brackets; dots are for object attributes.
  • Mutable default arguments — never write def f(items=[]); the list is shared across calls. Use def f(items=None): items = items or [].
  • Integer vs float division5 / 2 == 2.5 (float), 5 // 2 == 2 (floor). JS only has one /.
  • is vs ==== compares values; is compares identity. Use is only for None/singletons.
  • No ++ — use i += 1.
  • Truthy empties[], {}, "", 0, None are all falsy; check if 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.
  • None is your null; use is None and .get() to stay safe.
  • @dataclass is your typed record; type hints are optional TS-style annotations checked by mypy.
  • 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.