Syntax, types, and control flow
Dynamic vs strong typing, the built-in types, comprehensions, and structural pattern matching.
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
- Use Python's built-in types fluently.
- Read and write idiomatic control flow.
- Apply structural pattern matching (3.10+).
- Avoid the most common type / operator pitfalls.
- 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).
n: int = 42 # type annotation (not enforced at runtime)
pi: float = 3.14159
name: str = "Ada"
ok: bool = True
nothing: None = NoneType checks:
isinstance(n, int) # True
isinstance(True, int) # True โ bool subclasses int
type(n) is int # TrueConvert:
int("42"), float("3.14"), str(42), bool("") # 42, 3.14, "42", False
int("0xff", 16) # parse with basebool "truthiness" rules: False, None, 0, 0.0, "", [], {}, set() are falsy; everything else is truthy.
2. Numbers
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
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 literalf-strings (your default since 3.6)
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":
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:
_MISSING = object()
def get(d, k, default=_MISSING):
if k in d:
return d[k]
if default is _MISSING:
raise KeyError(k)
return default5. Operators
| Category | Operators |
|---|---|
| Arithmetic | + - * / // % ** |
| Comparison | == != < <= > >= |
| Logical | and or not (short-circuit) |
| Bitwise | & | ^ ~ << >> |
| Identity | is, is not |
| Membership | in, not in |
| Walrus | := (3.8+) |
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
"" 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
if x > 0:
print("positive")
elif x < 0:
print("negative")
else:
print("zero")Indentation is the syntax. 4 spaces, no tabs.
Ternary
status = "ok" if x > 0 else "bad"for
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):
passwhile
while not done:
do_thing()break, continue, else
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+)
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 xbinds tox. - Class:
case Point(x=0)(matches Point withx == 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).
# 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
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
# 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
Mutable default argument:
pythondef append(x, target=[]): # SHARED across calls! target.append(x) return targetFix:
pythondef append(x, target=None): target = [] if target is None else target target.append(x) return targetInteger division
/vs//:1 / 2is0.5, not0. Use//for floor.Float comparison:
python0.1 + 0.2 == 0.3 # False math.isclose(0.1 + 0.2, 0.3) # Trueisvs==:ischecks identity (same object).==checks equality. Almost always use==.Late binding in closures:
pythonfuncs = [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 (
ruffdefault; PEP 8 says 79 but everyone uses 100 now). - One statement per line.
Let ruff format handle it.
Hands-on lab (1.5 hours)
- Write a function
fizzbuzz(n)returning a list of lengthnwith FizzBuzz rules. - Implement temperature conversion
to_celsius(f)andto_fahrenheit(c). - Use
matchto parse a list of simple commands:["ping"], ["echo", "hi"], ["add", 1, 2]. - Find the most-common word in a paragraph (
collections.Counter). - Compare
isand==for: small int, large int, list, tuple, intern'd string, dynamically built string. - Trigger the mutable-default bug, then fix it.
- Bonus: write a one-liner using
walrusto read lines from stdin until EOF, accumulating non-empty ones.
Self-check
- List five truthy and five falsy values.
- What does
divmod(7, 2)return? - EAFP vs LBYL โ which does Python prefer?
- How do you swap two variables without a temp?
- 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.