Whetstone.
The Spellingdef, class, and the block
Module 1 · Lesson 115 min

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.

Three declarations, one of which has no keyword
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.
    pass

That 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.

The colon opens, the indentation holds
def classify(score):
    if score > 90:
        return "excellent"
    elif score > 50:
        return "fine"
    else:
        return "bad"

Four things in that snippet worth naming:

  • elif, not else if. It is one word and it is not optional. else if on 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

Parameters, defaults, keywords, and the two star forms
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:

Named arguments are free and universal
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:

  • return with no value, or falling off the end, both give None. There is no undefined.
  • ... is a real expression (it is the Ellipsis singleton) and is idiomatic as a body placeholder in stubs and protocols. pass is 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.

Both of these are locals of the whole function
def handle(items):
    for item in items:
        latest = item          # bound inside the loop
    return latest              # ...and still visible here

That 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

The three import forms and the one incantation
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.

Practice

Try it yourself

Quiz

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?

  1. Apass, which is a statement that exists purely to satisfy the parser
  2. BNothing at all, because an empty indented block is legal and simply does nothing
  3. Ccontinue, which moves execution past the block without running it
  4. Dreturn None, which is the only way to end a body that produces no value
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.

Quiz

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?

  1. Alabel = count > 0 ? "some" : "none"
  2. Blabel = "some" if count > 0 else "none"
  3. Clabel = if count > 0: "some" else: "none"
  4. Dlabel = count > 0 and "some" or "none"
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.

Quiz

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?

  1. AIt is visible but always None, because leaving a block rebinds any name assigned inside of it
  2. BIt is out of scope, exactly as a let declared inside an if block is out of scope in TypeScript
  3. CIt is visible for the rest of the function, since Python scopes names per function, not per block
  4. DIt is visible only if the if branch ran, and referencing it otherwise is a SyntaxError at import
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.

Recall

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.

Check

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 should see

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.

Sign in to track your progress →