Syntax, types, and control flow

Dynamic vs strong typing, the built-in types, comprehensions, and structural pattern matching.

๐Ÿ Module 1 8 min read Not started

Why this matters

You need to be fluent in the basics โ€” types, operators, conditionals, loops โ€” without thinking about syntax. This lesson is a rapid tour for those who've programmed in any other language; you should be able to read everything here at a glance after going through it once.

Learning objectives

  1. Use Python's built-in types fluently.
  2. Read and write idiomatic control flow.
  3. Apply structural pattern matching (3.10+).
  4. Avoid the most common type / operator pitfalls.
  5. Use f-strings for all formatting.

1. Variables and types

Python is dynamically typed (every value has a type, names don't) and strongly typed (no implicit "3" + 5).

python
n: int = 42                 # type annotation (not enforced at runtime)
pi: float = 3.14159
name: str = "Ada"
ok: bool = True
nothing: None = None

Type checks:

python
isinstance(n, int)         # True
isinstance(True, int)      # True โ€” bool subclasses int
type(n) is int             # True

Convert:

python
int("42"), float("3.14"), str(42), bool("")     # 42, 3.14, "42", False
int("0xff", 16)            # parse with base

bool "truthiness" rules: False, None, 0, 0.0, "", [], {}, set() are falsy; everything else is truthy.


2. Numbers

python
1 + 2                       # 3
7 / 2                       # 3.5    (true division)
7 // 2                      # 3      (floor division)
7 % 2                       # 1
2 ** 10                     # 1024   (power)
divmod(7, 2)                # (3, 1)

1_000_000                   # underscores for readability
0xff, 0b1010, 0o17          # bases

import math
math.isclose(0.1 + 0.2, 0.3)  # True โ€” floats compare with tolerance

from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")    # True; arbitrary precision

from fractions import Fraction
Fraction(1, 3) + Fraction(1, 6)        # Fraction(1, 2)

For money / scientific exactness, use Decimal. For rational math, Fraction. For everyday calculations, float is fine.


3. Strings

python
s = "hello"
s.upper(), s.title(), s.lower()
s.startswith("he"), s.endswith("lo"), "ll" in s
s.split(), "a-b-c".split("-"), ", ".join(["a", "b"])
s.strip(), s.replace("l", "L"), s.find("l"), s.count("l")

# Slicing
s[0], s[-1], s[1:4], s[::-1], s[::2]

# Multi-line and raw
"""line 1
line 2"""
r"C:\Users\Ada"             # raw โ€” backslash literal

f-strings (your default since 3.6)

python
name, age = "Ada", 30
f"Hi, {name}, age {age}"
f"{1.234567:.2f}"           # "1.23"
f"{1000000:,}"              # "1,000,000"
f"{0.85:.0%}"               # "85%"
f"{name=}"                  # debug form: "name='Ada'"

3.12+ allows \n and nested quotes inside f-strings โ€” fewer hoops.


4. None, sentinels, optionals

None is the only value of type NoneType. Use it as "absent" / "uninitialised":

python
def find_user(uid: int) -> User | None:
    ...

user = find_user(42)
if user is None:                # always use `is` for None
    raise ValueError("not found")

When None is a valid value, use a unique sentinel:

python
_MISSING = object()
def get(d, k, default=_MISSING):
    if k in d:
        return d[k]
    if default is _MISSING:
        raise KeyError(k)
    return default

5. Operators

CategoryOperators
Arithmetic+ - * / // % **
Comparison== != < <= > >=
Logicaland or not (short-circuit)
Bitwise& | ^ ~ << >>
Identityis, is not
Membershipin, not in
Walrus:= (3.8+)
python
x = 5
0 < x < 10                  # chained comparison โ€” Pythonic
1 == 1.0                    # True (numeric equality)
True == 1                   # True (bool subclasses int)

# Walrus: assign-in-expression
if (n := len(data)) > 100:
    print(f"big: {n}")

Truth-table shortcuts

python
"" or "default"             # "default"
"abc" or "default"          # "abc"
0 and "anything"            # 0
"abc" and "second"          # "second"

and / or return the value, not just True/False. Useful for defaults.


6. Control flow

if / elif / else

python
if x > 0:
    print("positive")
elif x < 0:
    print("negative")
else:
    print("zero")

Indentation is the syntax. 4 spaces, no tabs.

Ternary

python
status = "ok" if x > 0 else "bad"

for

python
for item in [1, 2, 3]:
    print(item)

for i, item in enumerate(["a", "b"], start=1):
    print(i, item)

for key, value in {"a": 1, "b": 2}.items():
    print(key, value)

for x, y in zip([1, 2, 3], ["a", "b", "c"]):
    print(x, y)

for x, y in zip([1, 2], ["a", "b", "c"], strict=True):    # 3.10+
    print(x, y)             # raises ValueError if lengths differ

for i in range(10):
    pass

while

python
while not done:
    do_thing()

break, continue, else

python
for x in items:
    if x is None:
        continue
    if found(x):
        break
else:
    # runs if the loop completed without `break`
    print("not found")

for ... else is rare but elegant. Same on while.

match (structural pattern matching, 3.10+)

python
def handle(msg):
    match msg:
        case {"type": "ping"}:
            return "pong"
        case {"type": "echo", "text": str(t)}:
            return t
        case [first, *rest] if len(rest) > 0:
            return f"list starting with {first}"
        case Point(x=0, y=0):
            return "origin"
        case _:
            return "unknown"

Patterns:

  • Literal: case 0, case "ok".
  • Capture: case x binds to x.
  • Class: case Point(x=0) (matches Point with x == 0).
  • Sequence: case [1, 2, *rest].
  • Mapping: case {"key": value}.
  • Or: case 1 | 2 | 3.
  • Guard: case x if x > 0.

match shines for parser-like dispatch (JSON, AST traversal, command handling).


7. Common idioms

EAFP vs LBYL

Python culture prefers "Easier to Ask Forgiveness than Permission" (try and catch) over "Look Before You Leap" (check first).

python
# EAFP (Pythonic)
try:
    value = d["key"]
except KeyError:
    value = "default"

# LBYL (less Pythonic, has a race condition in concurrent code)
if "key" in d:
    value = d["key"]
else:
    value = "default"

# Best for dicts: use .get()
value = d.get("key", "default")

Multiple assignment / swap

python
a, b = 1, 2
a, b = b, a                # swap; no temp
x, *rest = [1, 2, 3, 4]    # x = 1, rest = [2, 3, 4]
first, *middle, last = range(10)

Comprehension vs loop

python
# Loop
squares = []
for x in range(10):
    squares.append(x * x)

# Comprehension โ€” preferred when simple
squares = [x * x for x in range(10)]

# With condition
evens = [x for x in range(10) if x % 2 == 0]

Don't over-stuff comprehensions; if it doesn't fit one readable line, use a loop.


8. Common pitfalls

  1. Mutable default argument:

    python
    def append(x, target=[]):   # SHARED across calls!
        target.append(x)
        return target

    Fix:

    python
    def append(x, target=None):
        target = [] if target is None else target
        target.append(x)
        return target
  2. Integer division / vs //: 1 / 2 is 0.5, not 0. Use // for floor.

  3. Float comparison:

    python
    0.1 + 0.2 == 0.3           # False
    math.isclose(0.1 + 0.2, 0.3)   # True
  4. is vs ==: is checks identity (same object). == checks equality. Almost always use ==.

  5. Late binding in closures:

    python
    funcs = [lambda: i for i in range(3)]
    [f() for f in funcs]       # [2, 2, 2], not [0, 1, 2]
    # Fix: lambda i=i: i

9. PEP 8 essentials

  • 4-space indent.
  • Snake_case for variables/functions, PascalCase for classes, UPPER_SNAKE for constants.
  • 2 blank lines between top-level definitions, 1 between methods.
  • Imports grouped (stdlib, third-party, first-party) with blank lines.
  • 100-char lines (ruff default; PEP 8 says 79 but everyone uses 100 now).
  • One statement per line.

Let ruff format handle it.


Hands-on lab (1.5 hours)

  1. Write a function fizzbuzz(n) returning a list of length n with FizzBuzz rules.
  2. Implement temperature conversion to_celsius(f) and to_fahrenheit(c).
  3. Use match to parse a list of simple commands: ["ping"], ["echo", "hi"], ["add", 1, 2].
  4. Find the most-common word in a paragraph (collections.Counter).
  5. Compare is and == for: small int, large int, list, tuple, intern'd string, dynamically built string.
  6. Trigger the mutable-default bug, then fix it.
  7. Bonus: write a one-liner using walrus to read lines from stdin until EOF, accumulating non-empty ones.

Self-check

  1. List five truthy and five falsy values.
  2. What does divmod(7, 2) return?
  3. EAFP vs LBYL โ€” which does Python prefer?
  4. How do you swap two variables without a temp?
  5. What is the walrus operator?

References

  • Python tutorial: https://docs.python.org/3/tutorial/
  • PEP 8 โ€” Style Guide.
  • PEP 634 โ€” Structural Pattern Matching.
  • Fluent Python, Ramalho โ€” Chapters 2-3.
  • Effective Python, Brett Slatkin โ€” Items 1-30.

Sign in to save your progress and earn badges.