Freebie

Fundamentals

This page covers Python’s core building blocks: operators, control flow, and loops. Understanding where Python differs from other languages — chained comparisons, loop else clauses, match/case — pays dividends throughout the rest of the language.

Operators

Comparison and assignment operators

The following operators form the foundation of Python expressions:

=       # assignment — binds a name to a value
==      # equality — compares values, not memory location

# Identity — compares memory location, not value
is      # True if both names refer to the same object
is not  # True if both names refer to different objects

# Membership
in      # True if x is in y
not in  # True if x is not in y

# Logical
and
or
not

# Boolean literals
True
False

To test equality between values, apply ==. To test whether two names refer to the same object in memory, apply is. None checks should always apply is rather than ==:

value = None
if value is None:
    print("no value provided")

Augmented assignments

Augmented assignment operators combine an arithmetic operation with assignment. They update a variable in place rather than creating a new binding:

# Without augmented assignments:
count = count + 1
total = total - refund

# With augmented assignments — shorter and reads as "add to," "subtract from":
count += 1
total -= refund
total *= 1.08   # apply sales tax
rate  /= 100    # convert percentage to decimal

The following example tallies HTTP response codes. Updating counters this way is idiomatic Python and appears frequently in metrics collection, log analysis, and retry logic:

from dataclasses import dataclass, field

@dataclass
class RequestStats:
    total: int = 0
    success: int = 0
    client_errors: int = 0
    server_errors: int = 0

def tally_response(stats: RequestStats, status: int) -> None:
    stats.total += 1

    if 200 <= status <= 299:
        stats.success += 1
    elif 400 <= status <= 499:
        stats.client_errors += 1
    elif 500 <= status <= 599:
        stats.server_errors += 1

responses = [200, 201, 404, 500, 200, 403, 502]
stats = RequestStats()
for code in responses:
    tally_response(stats, code)

print(stats)
# RequestStats(total=7, success=3, client_errors=2, server_errors=2)

Chained comparisons

Python allows chained comparisons that read like mathematical range notation. The expression 200 <= status <= 299 is equivalent to status >= 200 and status <= 299, but more readable and evaluates the middle operand only once. Most other languages do not support this syntax:

status = 201

# Chained comparison — unambiguous range check:
if 200 <= status <= 299:
    print("success")

# Works for any comparable type — useful for grading, thresholds, or validation:
score = 85
if 60 <= score < 70:
    grade = "D"
elif 70 <= score < 80:
    grade = "C"
elif 80 <= score < 90:
    grade = "B"
else:
    grade = "A"

Control flow

if, elif, else

Python evaluates if/elif/else branches in order and runs the first branch whose condition is True. Only one branch runs:

name = 'Smith'

if name == 'Nelson':
    print('This is wrong')
elif name == 'Douglas':
    print('This is wrong')
elif name == 'Smith':
    print('This is right')
else:
    print('No answer')

Ternary expressions

A ternary expression (also called a conditional expression) returns one of two values depending on a condition. Apply it when the entire expression fits on one readable line and there are exactly two outcomes:

import os

env = os.getenv("APP_ENV", "development")

# Without ternary — four lines for a simple label:
# if env == "development":
#     log_level = "DEBUG"
# else:
#     log_level = "WARNING"

# With ternary — one line, reads naturally:
log_level = "DEBUG" if env == "development" else "WARNING"

db_url = os.getenv("DATABASE_URL") if env == "production" else "sqlite:///dev.db"

Avoid chaining ternary expressions. When a condition has more than two outcomes, an if/elif/else block is clearer.

match / case

The match/case statement (Python 3.10 and later) replaces long if/elif chains and supports structural pattern matching — matching on values, types, and the shape of objects. The following example routes incoming HTTP requests to handler functions:

from typing import TypedDict

class RouteResult(TypedDict):
    handler: str
    allow_body: bool

def route_request(method: str, path: str) -> RouteResult:
    match method.upper():
        case "GET" | "HEAD":
            return {"handler": f"read_{path.strip('/').replace('/', '_')}", "allow_body": False}
        case "POST":
            return {"handler": f"create_{path.strip('/').replace('/', '_')}", "allow_body": True}
        case "PUT" | "PATCH":
            return {"handler": f"update_{path.strip('/').replace('/', '_')}", "allow_body": True}
        case "DELETE":
            return {"handler": f"delete_{path.strip('/').replace('/', '_')}", "allow_body": False}
        case _:
            raise ValueError(f"Unsupported HTTP method: {method}")

print(route_request("GET", "/users"))    # {'handler': 'read_users', 'allow_body': False}
print(route_request("POST", "/orders"))  # {'handler': 'create_orders', 'allow_body': True}

The _ case is the default — it matches anything not matched above. The | operator matches multiple values in a single case.

Loops

while loop

A while loop runs as long as its condition is True. The following example counts from 0 to 4:

count = 0
while count < 5:
    print(count)
    count += 1
# 0 1 2 3 4

break, continue, and else in loops

break exits a loop immediately. continue skips the rest of the current iteration and moves to the next one. The else clause on a loop runs only when the loop completes without hitting a break — making it useful for “search, and report not found” patterns.

The following example searches a list of database servers for the first one that accepts a connection. The else block runs only when every server failed:

import socket
from typing import Optional

SERVERS = [
    ("db-primary.internal", 5432),
    ("db-replica-1.internal", 5432),
    ("db-replica-2.internal", 5432),
]

def find_healthy_server(timeout: float = 1.0) -> Optional[tuple[str, int]]:
    for host, port in SERVERS:
        try:
            with socket.create_connection((host, port), timeout=timeout):
                print(f"Connected to {host}:{port}")
                break  # found a healthy server — stop searching
        except (socket.timeout, ConnectionRefusedError, OSError):
            continue   # this server is down — try the next one
    else:
        # Runs only when the loop exhausted all servers without hitting break.
        return None

    return (host, port)

for and in with iterators

The for/in loop iterates over any iterable — an object that produces values one at a time. Strings, lists, tuples, and dictionaries are all iterables:

sentence = 'A string is an iterable'
for letter in sentence:
    print(letter)

enumerate()

When you need both the position and the value during iteration, apply enumerate() rather than tracking an index variable manually. It yields (index, value) pairs. The following example builds a side-by-side diff of two configuration files:

def build_diff_lines(original: list[str], revised: list[str]) -> None:
    # enumerate(iterable, start=1) yields (line_number, value) pairs.
    # zip() pairs the two lists by position, stopping at the shorter one.
    for lineno, (old, new) in enumerate(zip(original, revised), start=1):
        if old != new:
            print(f"  line {lineno:>4}: - {old.rstrip()}")
            print(f"  line {lineno:>4}: + {new.rstrip()}")

v1 = ["host: localhost\n", "port: 5432\n", "pool_size: 10\n"]
v2 = ["host: db.prod.internal\n", "port: 5432\n", "pool_size: 25\n"]
build_diff_lines(v1, v2)
# line    1: - host: localhost
# line    1: + host: db.prod.internal
# line    3: - pool_size: 10
# line    3: + pool_size: 25

range() to generate numbers

range() returns a stream of integers within a specified range without allocating a sequence in memory. It returns an iterable object you can step through with a for/in loop or convert to a list:

# range(start, stop, step)

# Standard iteration from 0 to 4:
for x in range(0, 5):
    print(x)
# 0 1 2 3 4

# Convert to a list:
list(range(0, 5))
# [0, 1, 2, 3, 4]

# Count down with a negative step:
for x in range(5, -1, -1):
    print(x)
# 5 4 3 2 1 0