def, class, and the block
Right. The actual answer to the actual question, before anything else.
def is for functions. class is for classes. Variables have no keyword at all: you just assign to a name.
MAX_RETRIES = 3 # a variable. No let, no const, no var. Just a name and a value.
def send(payload): # a function.
return post(payload)
class Sender: # a class.
passThat is the whole thing you could not remember, and it is worth noticing how little there was to remember. This lesson is the rest of the spelling table, because the same shape of gap is sitting behind about fifteen other keywords, and every one of them is a five-second fix that costs you twenty minutes when you hit it cold in someone else’s repo.
The block is whitespace, and the colon opens it
No braces. A colon at the end of a line says a block starts here, and the block is everything indented under it.
def classify(score):
if score > 90:
return "excellent"
elif score > 50:
return "fine"
else:
return "bad"Four things in that snippet worth naming:
elif, notelse if. It is one word and it is not optional.else ifon one line is a syntax error.- Four spaces. Not a rule of the language, which accepts any consistent indentation, but the entire ecosystem does four spaces and every formatter enforces it. Do not be interesting here.
- No semicolons. They are legal as statement separators and nobody uses them.
- No parentheses around the condition. Legal, pointless, and the formatter will not remove them, so they will just sit there advertising that you came from somewhere else.
And when a block must exist but has nothing in it, you write pass. It is the {} you cannot write, and it does nothing at all, deliberately.
Functions
def request(url, method="GET", *, timeout=30, retries=2, **headers):
...url is required and positional. method has a default. The bare * says everything after me must be passed by keyword, which is how library authors keep a nine-parameter constructor callable without a mystery-argument minefield. **headers collects any leftover keyword arguments into a dict, and *args (which is not in that signature) collects leftover positional ones into a tuple.
The caller side is where Python is genuinely nicer than what you are used to:
request("https://example.com", timeout=5, retries=0, Authorization="Bearer ...")Every function in Python takes named arguments, for free, without the author designing an options object. This is why Python APIs have long flat signatures where a TypeScript API would take a config object: the language already gave them the ergonomics you were building the object for.
Two more small things:
returnwith no value, or falling off the end, both giveNone. There is noundefined....is a real expression (it is theEllipsissingleton) and is idiomatic as a body placeholder in stubs and protocols.passis the general-purpose one;...reads as deliberately unimplemented.
Everything that is a symbol in TypeScript and a word in Python
This is the table. It is short, and it is most of your fluency gap.
| TypeScript | Python | Note |
|---|---|---|
&& || ! |
and or not |
Words. and and or return an operand, not a boolean, exactly like && and || do. |
cond ? a : b |
a if cond else b |
Reversed. The single most-fumbled line. |
null / undefined |
None |
One of them. None is a singleton; compare with is None, never == None. |
true / false |
True / False |
Capitalised. |
=== |
== |
Python’s == does not do type coercion, so it is already what you wanted === for. |
Object.is / reference equality |
is |
Identity. Use it for None, True, False, and nothing else. |
x instanceof Y |
isinstance(x, Y) |
A function. |
"a" in obj |
"a" in d |
Same idea, and it also works on lists, strings, sets and anything iterable. |
typeof x |
type(x) |
Returns the class object, not a string. |
| Template literal | f"hi {name}" |
An f-string. The f prefix is mandatory; without it the braces are literal text. |
// comment |
# comment |
There is no block comment. |
throw |
raise |
|
catch (e) |
except SomeError as e: |
You name the exception type. A bare except: catches everything including KeyboardInterrupt and is almost always a bug. |
finally |
finally |
Unchanged. |
switch |
match / case |
Since 3.10, and it is structural pattern matching rather than a jump table. Rarer than you would expect; most Python code still uses if/elif. |
x++ |
x += 1 |
There is no increment operator. At all. |
function () {} arrow |
lambda x: x + 1 |
One expression only. No statements, no multi-line body. |
Scope, and the thing that will bite you
Python has function scope and module scope. There is no block scope.
def handle(items):
for item in items:
latest = item # bound inside the loop
return latest # ...and still visible hereThat is legal and it does what it looks like. It is also a live grenade: if items is empty, the loop body never runs, latest was never bound, and the return raises UnboundLocalError at runtime. TypeScript would have refused to compile this. Python will happily ship it and fail on the one input where the list came back empty.
nonlocal and global exist to write to a name from an enclosing scope, and you will see them roughly never in application code.
Naming, because the ecosystem is unusually strict about it
snake_case for functions, variables, methods and modules. PascalCase for classes. SCREAMING_SNAKE for constants. This is not taste, it is PEP 8 and every linter in the language enforces it, so camelCase in a Python file reads exactly the way snake_case reads in a TypeScript file.
And there is no private. The convention is a leading underscore meaning internal, do not touch, enforced by nothing but manners. A double leading underscore triggers name mangling inside classes, which is a different and rarer thing you can safely ignore for now.
Imports, and the bottom of every file
import json # the whole module. json.loads(...)
from pathlib import Path # one name out of a module. Path(...)
from typing import Any, Optional # several names
import numpy as np # aliased
def main():
...
if __name__ == "__main__":
main()That last block is the closest thing Python has to a folk ritual, and it is worth understanding rather than copying. __name__ is a string every module carries. It is the module’s import name normally, and the literal "__main__" when the file is the one being executed directly. So the block means run this only when this file is the entry point, not when someone imports it. Without it, importing your script to reuse one function would run the whole thing.
Where this goes
You now have the shape of a Python file and the spelling of every keyword you will type in the first hour. The next lesson does the data structures, which is where Python starts being noticeably better than what you are used to rather than merely different.
Try it yourself
The empty block
You are sketching a class and you want a method body that does nothing yet, so the file still parses. What goes in the body?
Show answer
Correct answer: A — pass, which is a statement that exists purely to satisfy the parser
pass is the placeholder, and it exists because Python has no braces to leave empty. Nothing at all is a IndentationError: the parser requires at least one statement in a block. continue is a loop control statement and is a SyntaxError outside a loop. return None does parse and does run, but it is a real statement with real semantics rather than a placeholder, and in a class body rather than a method it would be a SyntaxError too.
The conditional expression
You want the Python equivalent of this TypeScript line, which picks a label from a count. In TypeScript you would write a ? and a :. What is the Python spelling?
Show answer
Correct answer: B — label = "some" if count > 0 else "none"
Python's conditional expression puts the value first and the test in the middle, which is the reverse of the C-family order and the single most common thing to get backwards in your first week. There is no ?: operator in Python at all. The if/else statement form cannot appear on the right of an assignment because it is a statement, not an expression. The and/or chain does produce the right answer here and was the genuine idiom before Python 2.5, but it silently returns the wrong branch whenever the true-value is falsy, which is exactly why the conditional expression was added.
Where a name lives after its block
Consider a function containing if flag: and then an indented result = compute(). After the if block ends, what is true about result?
Show answer
Correct answer: C — It is visible for the rest of the function, since Python scopes names per function, not per block
Python has function scope and module scope, and no block scope at all. A name bound anywhere in a function is a local of that whole function, so result is visible after the block. The out-of-scope option is the TypeScript instinct and is the reason this trips people up. The SyntaxError option is close to a real behaviour but names the wrong error: if the branch did not run, the name was never bound and reading it raises UnboundLocalError at runtime, which is not a SyntaxError and will not be caught before you ship.
Keyword-only arguments
Worth knowing cold, because the agent libraries you are about to read use this constantly to keep long constructor signatures readable.
How do you declare that some of a function's parameters may only be passed by keyword, and what does the marker look like in the signature?
Reveal answer
Put a bare * in the parameter list. Everything after it is keyword-only, so def create_agent(model, *, tools=None, checkpointer=None) allows create_agent(m, tools=[...]) and rejects create_agent(m, [...]) with a TypeError. A *args parameter has the same effect on everything after it. The mirror image is /, which marks everything before it as positional-only. This is why library signatures you read will often look like they have a stray asterisk in the middle: it is a fence, not a parameter.
Write the spelling table
In a scratch file, write the TypeScript form and the Python form side by side for ten things from this lesson. Do it from memory first, then check.
You have ten pairs, you wrote the conditional expression with the value first, and you did not write a single semicolon or curly brace on the Python side.