Go — Core Language
Types, functions, structs, interfaces
Variables and Type Inference
Go is statically typed but uses type inference to eliminate boilerplate. The `:=` short declaration infers the type from the right-hand side at compile time. `var` declarations with explicit types are used at package level or when the zero value is intentional.
package main
import "fmt"
func main() {
x := 42 // int inferred
name := "Go" // string inferred
ratio := 3.14 // float64 inferred
active := true // bool inferred
var count int // zero value: 0
var label string // zero value: ""
fmt.Println(x, name, ratio, active, count, label)
}Basic Types
Go has a small, fixed set of built-in types. Integer types are explicitly sized (`int8`, `int16`, `int32`, `int64`, `uint8` … `uint64`), plus `int` and `uint` which are platform-width (64-bit on 64-bit systems). `string` is an immutable sequence of bytes (UTF-8). `rune` is an alias for `int32`, representing a Unicode code point.
var i int = 100 var u uint32 = 4294967295 var f float64 = 3.141592653589793 var b byte = 255 // alias for uint8 var r rune = '🚀' // alias for int32, holds Unicode code point var s string = "Hello, 世界" var ok bool = true
Functions
Functions are first-class values in Go. They can return multiple values — idiomatic Go uses this for `(result, error)` pairs instead of exceptions. Functions can be assigned to variables, passed as arguments, and defined as closures capturing their surrounding scope.
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Named return values (document what each value means)
func minMax(nums []int) (min, max int) {
min, max = nums[0], nums[0]
for _, n := range nums[1:] {
if n < min { min = n }
if n > max { max = n }
}
return // naked return
}
// Function as value
double := func(x int) int { return x * 2 }Control Flow
Go has `if`, `for`, and `switch` — no `while`, no `do-while`, no `until`. `for` covers all looping: classic C-style, while-style, and range-based. `switch` does not fall through by default (no `break` needed); use `fallthrough` explicitly when required.
// if with initialization statement
if err := doWork(); err != nil {
log.Fatal(err)
}
// for as while
for running {
process()
}
// range over slice (index, value)
for i, v := range items {
fmt.Println(i, v)
}
// switch — no fall-through by default
switch status {
case 200: fmt.Println("OK")
case 404: fmt.Println("Not Found")
default: fmt.Println("Other")
}Structs
Go uses structs for composite data types. There is no `class` keyword. Methods are defined outside the struct body using a receiver syntax. Structs are value types — assigning one copies it; use a pointer receiver when a method needs to mutate state or the struct is large.
type Point struct {
X, Y float64
}
// Value receiver — reads state
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// Pointer receiver — mutates state
func (p *Point) Scale(factor float64) {
p.X *= factor
p.Y *= factor
}
p := Point{X: 3, Y: 4}
fmt.Println(p.Distance()) // 5
p.Scale(2)
fmt.Println(p) // {6 8}Interfaces
Interfaces in Go are satisfied implicitly — a type implements an interface simply by having the required methods, with no `implements` keyword. This enables loose coupling: packages define small interfaces, and any type that happens to match can be used. The most famous Go interface is `io.Reader` with its single `Read([]byte) (int, error)` method.
type Stringer interface {
String() string
}
type Temperature struct {
Celsius float64
}
// Temperature implicitly satisfies Stringer
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", t.Celsius)
}
func Print(s Stringer) {
fmt.Println(s.String())
}
Print(Temperature{36.6}) // 36.6°CPointers
Go has pointers but no pointer arithmetic. A pointer holds the memory address of a value. Use `&` to take an address, `*` to dereference. Pointers are primarily used for: mutating a value through a function argument, sharing large structs without copying, and expressing optional values (nil pointer = absent).
x := 10
p := &x // p is *int, holds address of x
*p = 20 // dereference: x is now 20
fmt.Println(x) // 20
// nil pointer — optional value pattern
type Node struct {
Value int
Next *Node // nil when there is no next node
}Arrays and Slices
An array in Go has a fixed length that is part of its type: `[3]int` and `[4]int` are different types. In practice, arrays are rarely used directly — **slices** are the workhorse. A slice is a descriptor (pointer + length + capacity) over an underlying array. `make([]T, len, cap)` allocates one. `append` grows slices automatically.
// Array (fixed size, value type)
arr := [3]int{1, 2, 3}
// Slice (dynamic, reference type)
s := []int{10, 20, 30}
s = append(s, 40)
// Slice of slice
sub := s[1:3] // [20, 30] — shares underlying array
// make with capacity hint
buf := make([]byte, 0, 4096)Maps
A `map[K]V` is Go's built-in hash map. Keys must be `comparable` (no slices, maps, or functions as keys). The zero value of a map is `nil` — attempting to write to a nil map panics. Always initialize with `make` or a literal. The two-value assignment `v, ok := m[key]` safely checks for key presence without panicking on missing keys.
// Literal initialization
scores := map[string]int{
"Alice": 95,
"Bob": 87,
}
// Safe lookup
score, ok := scores["Charlie"]
if !ok {
fmt.Println("not found")
}
// Delete
delete(scores, "Bob")
// Iterate (order is random)
for name, s := range scores {
fmt.Println(name, s)
}Error Handling
Go uses explicit error return values instead of exceptions. The `error` interface has one method: `Error() string`. Functions signal failure by returning a non-nil error as their last return value. `fmt.Errorf("context: %w", err)` wraps errors with context; `errors.Is` and `errors.As` unwrap them for inspection.
import "errors"
var ErrNotFound = errors.New("not found")
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("findUser: invalid id %d", id)
}
user := db.Lookup(id)
if user == nil {
return nil, fmt.Errorf("findUser %d: %w", id, ErrNotFound)
}
return user, nil
}
// Caller
user, err := findUser(42)
if errors.Is(err, ErrNotFound) {
// handle not-found specifically
}Defer
`defer` schedules a function call to run when the surrounding function returns — regardless of whether it returns normally or via `panic`. Multiple defers execute in LIFO order. The primary use is resource cleanup: closing files, releasing locks, closing database connections.
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close() // guaranteed to run when copyFile returns
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}Closures
A closure is a function that captures variables from its enclosing scope by reference. In Go, closures are commonly used to create stateful function values, implement middleware, and build iterators. Since Go 1.22, range loop variables create fresh bindings per iteration, eliminating the classic closure-in-loop pitfall.
func makeCounter() func() int {
n := 0
return func() int {
n++
return n
}
}
c := makeCounter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
// Each counter has independent state
d := makeCounter()
fmt.Println(d()) // 1Embedding
Go's alternative to inheritance is struct embedding. Embedding a type promotes its methods to the outer struct, allowing composition-based reuse without the complexity of class hierarchies. The embedded type remains accessible by its type name if needed.
type Animal struct {
Name string
}
func (a Animal) Speak() string { return a.Name + " makes a sound" }
type Dog struct {
Animal // embedded — Dog inherits Speak
Breed string
}
func (d Dog) Speak() string { return d.Name + " barks" } // override
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Speak()) // Rex barks
fmt.Println(d.Animal.Speak()) // Rex makes a sound
fmt.Println(d.Name) // promoted field accessConstants and iota
Constants are immutable values fixed at compile time, declared with `const`. The `iota` identifier is a constant generator that starts at 0 within a `const` block and increments by one for each line — it is the idiomatic way to create enumerated values. Untyped constants in Go have arbitrary precision and adapt to whatever type the context requires.
const Pi = 3.14159 // untyped constant
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
)
// iota with expressions — byte-size units
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)Type Assertions
A type assertion extracts the concrete value stored inside an interface. `v, ok := i.(T)` is the safe two-value form — `ok` is `false` instead of panicking when the assertion fails. The single-value form `v := i.(T)` panics on mismatch, so reserve it for cases where the type is guaranteed.
var i any = "hello" // Safe form — never panics s, ok := i.(string) fmt.Println(s, ok) // hello true n, ok := i.(int) fmt.Println(n, ok) // 0 false // Unsafe form — panics if i does not hold a string s = i.(string)
Type Switches
A type switch is a control structure that branches on the dynamic type of an interface value. The special syntax `switch v := i.(type)` binds `v` to the concrete value in each case, with the right static type. It is the idiomatic way to handle a value that may be one of several types.
func describe(i any) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int: %d", v)
case string:
return fmt.Sprintf("string of length %d", len(v))
case bool:
return fmt.Sprintf("bool: %t", v)
case nil:
return "nil value"
default:
return fmt.Sprintf("unknown type %T", v)
}
}Variadic Functions
A variadic function accepts a variable number of trailing arguments of the same type, declared with `...T`. Inside the function the parameter is a slice `[]T`. An existing slice can be passed to a variadic parameter with the spread operator `slice...`.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6 — pass individual args
sum() // 0 — zero args is valid
nums := []int{4, 5, 6}
sum(nums...) // 15 — spread an existing sliceNamed Types and Type Definitions
`type Celsius float64` creates a new named type distinct from its underlying type — values are not interchangeable without an explicit conversion, which prevents whole classes of unit-mixing bugs. A *type alias* `type Byte = uint8` (with `=`) is different: it creates no new type, just another name. Named types can carry their own methods.
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToF() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
var c Celsius = 100
var f Fahrenheit = c.ToF() // 212
// var bad Fahrenheit = c // compile error: cannot use c (Celsius) as Fahrenheit
type Byte = uint8 // alias — Byte and uint8 are the SAME typeinit Functions
`init` is a special function that runs automatically before `main`, after all package-level variables are initialized. A package (or even a single file) may declare multiple `init` functions; they run in the order they appear. `init` takes no arguments and returns nothing — it cannot be called directly.
var config map[string]string
func init() {
// Runs once, before main, after package vars are set
config = map[string]string{
"env": "production",
"tier": "backend",
}
}
func main() {
fmt.Println(config["env"]) // production — already populated
}The Blank Identifier
The blank identifier `_` is a write-only placeholder that discards a value. Go requires every declared variable and import to be used; `_` satisfies that rule when a value is intentionally unwanted — an unused return value, an unwanted range index, or an import needed only for its side effects.
// Discard the index, keep the value
for _, v := range items {
fmt.Println(v)
}
// Discard one of multiple returns
_, err := fmt.Println("hi")
// Import for side effects only (runs the package's init)
import _ "github.com/lib/pq"
// Compile-time interface check
var _ io.Writer = (*MyWriter)(nil)Panic and Recover
`panic` stops normal execution and begins unwinding the stack, running deferred functions along the way. `recover`, called inside a deferred function, stops the unwinding and returns the panic value — letting a program regain control. Go's convention is to use errors for expected failures and reserve `panic` for truly unrecoverable situations (programmer bugs, impossible states).
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
result = a / b // panics if b == 0
return result, nil
}
r, err := safeDivide(10, 0)
fmt.Println(r, err) // 0 recovered: runtime error: integer divide by zeroLabels and goto
Go supports statement labels used with `break`, `continue`, and `goto`. A labeled `break` or `continue` targets an *outer* loop — the cleanest way to exit a nested loop. `goto` jumps to a label within the same function; it is rarely used but occasionally clarifies error-cleanup paths or generated code.
grid := [][]int{{1, 2}, {3, 4}, {5, 6}}
target := 4
Outer:
for i, row := range grid {
for j, v := range row {
if v == target {
fmt.Printf("found at [%d][%d]\n", i, j)
break Outer // exits BOTH loops
}
}
}
// goto — jump to a label in the same function
i := 0
loop:
if i < 3 {
fmt.Println(i)
i++
goto loop
}Variables and Types
Python is dynamically typed — a variable is just a name bound to an object; the type lives on the object, not the variable. Everything is an object: integers, strings, functions, classes. Python's built-in types include `int`, `float`, `complex`, `bool`, `str`, `bytes`, `list`, `tuple`, `dict`, `set`, `frozenset`, and `NoneType`. `type()` returns an object's type; `isinstance()` checks inheritance.
x = 42 # int
y = 3.14 # float
z = 1 + 2j # complex
s = "hello" # str
b = b"bytes" # bytes
t = (1, 2, 3) # tuple (immutable)
lst = [1, 2, 3] # list (mutable)
d = {"a": 1} # dict
nothing = None # NoneType
print(type(x), isinstance(x, int)) # <class 'int'> True
print(type(s), len(s)) # <class 'str'> 5Control Flow
Python's control flow uses `if`/`elif`/`else`, `for` (iterating any iterable), `while`, `break`, `continue`, and `else` clauses on loops (the `else` runs if the loop wasn't broken out of). Conditional expressions (`x if cond else y`) provide ternary-style logic. Python has no `switch`; use `match`/`case` (3.10+) or dicts for dispatch tables.
# for/else — runs else only if loop wasn't broken
def find_prime(n: int) -> bool:
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
break
else:
return True # No divisor found
return False
print([x for x in range(2, 20) if find_prime(x)])
# [2, 3, 5, 7, 11, 13, 17, 19]
# Conditional expression
score = 85
grade = "pass" if score >= 60 else "fail"Functions — *args, **kwargs, and Defaults
Python functions support positional args, keyword args, defaults, variadic positional (`*args` → tuple), variadic keyword (`**kwargs` → dict), positional-only params (before `/`), and keyword-only params (after `*`). Functions are first-class: they can be passed, returned, and stored. The default value is evaluated once at definition time — a common gotcha with mutable defaults.
def create_connection(host, port=5432, /, *, timeout=30, **options):
# host, port: positional-only (before /)
# timeout: keyword-only (after *)
# options: extra keyword args
print(f"Connecting to {host}:{port} (timeout={timeout})")
print(f"Extra options: {options}")
create_connection("localhost", 5433, timeout=10, ssl=True, retry=3)
# *args and **kwargs forwarding
def wrapper(*args, **kwargs):
print(f"args={args}, kwargs={kwargs}")
return sum(args)
print(wrapper(1, 2, 3, label="sum"))Lambda Functions
`lambda` creates an anonymous single-expression function. It's syntactically limited — no statements, no docstrings, no multiple expressions — by design. Lambdas are most useful as short callbacks to `sorted()`, `map()`, `filter()`, or `functools.reduce()`. For anything more complex, a named `def` is clearer.
# Sort by multiple keys inline
employees = [
{"name": "Alice", "dept": "eng", "salary": 120000},
{"name": "Bob", "dept": "eng", "salary": 95000},
{"name": "Carol", "dept": "hr", "salary": 80000},
]
ranked = sorted(employees, key=lambda e: (e["dept"], -e["salary"]))
for e in ranked:
print(f"{e['dept']:5} {e['salary']:>7,} {e['name']}")
# Lambda with map and filter
nums = range(1, 11)
evens_sq = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, nums)))
print(evens_sq) # [4, 16, 36, 64, 100]Comprehensions
Python has four comprehension forms: list `[expr for x in it if cond]`, dict `{k: v for ...}`, set `{expr for ...}`, and generator `(expr for ...)`. Comprehensions are faster than equivalent `for` loops (the loop variable is local, avoiding global lookup) and more readable. Nested comprehensions are legal but keep nesting shallow — beyond two levels, a named generator is clearer.
# List, dict, set, generator comprehensions
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
evens = {x for x in flat if x % 2 == 0}
sq_map = {x: x**2 for x in range(1, 6)}
gen = (x**3 for x in range(1, 6)) # lazy
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(evens) # {2, 4, 6, 8}
print(sq_map) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
print(list(gen)) # [1, 8, 27, 64, 125]Generators and the `yield` Keyword
A generator function uses `yield` instead of `return` — calling it returns a generator object (an iterator). Execution pauses at each `yield` and resumes on the next `next()` call. Generators are memory-efficient for large sequences: instead of building a full list in memory, values are produced on demand. `yield from` delegates to a sub-generator, enabling generator pipelines and recursive generators.
import itertools
def fibonacci():
"""Infinite Fibonacci generator."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Take first 10 Fibonacci numbers
fibs = list(itertools.islice(fibonacci(), 10))
print(fibs) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
def read_in_chunks(file_obj, chunk_size=1024):
"""Generator that yields file content in fixed-size chunks."""
while chunk := file_obj.read(chunk_size):
yield chunkDecorators
A decorator is a callable that takes a function and returns a (usually modified) function. The `@decorator` syntax is syntactic sugar for `func = decorator(func)`. Decorators are used for cross-cutting concerns: logging, timing, access control, caching, retry logic, and input validation — without modifying the decorated function's body. `functools.wraps` preserves the wrapped function's metadata (`__name__`, `__doc__`).
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def retry(max_attempts=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
return wrapper
return decorator
@timer
@retry(max_attempts=3)
def fetch_data(url: str) -> str:
return f"data from {url}"
print(fetch_data("https://example.com"))Classes and Object-Oriented Programming
Python uses class-based OOP with dynamic dispatch. `__init__` is the initializer (not a constructor — `__new__` allocates the object). Instance attributes are set on `self`; class attributes are shared across instances. `@classmethod` receives the class as first arg; `@staticmethod` receives neither. Properties (`@property`) give attribute-style access to computed values.
class BankAccount:
interest_rate = 0.02 # class attribute
def __init__(self, owner: str, balance: float = 0.0):
self._owner = owner
self._balance = balance
@property
def balance(self) -> float:
return self._balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def withdraw(self, amount: float) -> None:
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
@classmethod
def from_dict(cls, data: dict) -> "BankAccount":
return cls(data["owner"], data.get("balance", 0.0))
def __repr__(self) -> str:
return f"BankAccount(owner={self._owner!r}, balance={self._balance:.2f})"
acc = BankAccount.from_dict({"owner": "Alice", "balance": 1000})
acc.deposit(500)
acc.withdraw(200)
print(acc) # BankAccount(owner='Alice', balance=1300.00)Inheritance and MRO
Python supports multiple inheritance. The Method Resolution Order (MRO) determines which class's method is called when multiple bases define the same name. Python uses the C3 linearization algorithm — `ClassName.__mro__` shows the order. `super()` follows the MRO, making cooperative multiple inheritance possible (all `__init__` methods in the hierarchy can be called in sequence using `super()`).
class A:
def who(self): return "A"
class B(A):
def who(self): return f"B -> {super().who()}"
class C(A):
def who(self): return f"C -> {super().who()}"
class D(B, C):
def who(self): return f"D -> {super().who()}"
d = D()
print(d.who()) # D -> B -> C -> A
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)Dunder (Magic) Methods
Dunder methods (`__method__`) let classes integrate with Python's built-in operators and protocols. `__repr__` / `__str__` control string representation. `__len__`, `__getitem__`, `__setitem__`, `__iter__` implement the sequence and mapping protocols. `__enter__` / `__exit__` enable the `with` statement. `__eq__`, `__lt__`, `__hash__` control equality and hashing. `__call__` makes instances callable.
class Vector:
def __init__(self, *components):
self._data = list(components)
def __repr__(self): return f"Vector{tuple(self._data)}"
def __len__(self): return len(self._data)
def __getitem__(self, i): return self._data[i]
def __add__(self, other): return Vector(*(a + b for a, b in zip(self, other)))
def __mul__(self, scalar): return Vector(*(x * scalar for x in self))
def __eq__(self, other): return self._data == list(other)
v1 = Vector(1, 2, 3)
v2 = Vector(4, 5, 6)
print(v1 + v2) # Vector(5, 7, 9)
print(v1 * 3) # Vector(3, 6, 9)
print(len(v1)) # 3
print(v1[1]) # 2Context Managers and `with`
The `with` statement guarantees cleanup code runs even if exceptions occur. Any object implementing `__enter__` / `__exit__` is a context manager. The `contextlib.contextmanager` decorator lets you write generator-based context managers — `yield` is the body of the `with` block. `contextlib.ExitStack` composes a dynamic number of context managers.
import contextlib
import time
@contextlib.contextmanager
def timed(label: str):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")
@contextlib.contextmanager
def managed_resource(name: str):
print(f"Acquiring {name}")
try:
yield name.upper()
finally:
print(f"Releasing {name}")
with timed("demo"):
with managed_resource("database") as res:
print(f"Using {res}")Exception Handling
Python exceptions are objects that inherit from `BaseException`. The hierarchy: `BaseException` → `Exception` → specific exceptions like `ValueError`, `TypeError`, `IOError`. The `try`/`except`/`else`/`finally` structure: `except` catches errors, `else` runs only if no exception occurred, `finally` always runs. Python 3.11+ added `ExceptionGroup` for handling multiple simultaneous exceptions (from `asyncio.TaskGroup`).
class InsufficientFundsError(ValueError):
def __init__(self, balance: float, amount: float):
self.balance = balance
self.amount = amount
super().__init__(f"Need {amount:.2f} but only have {balance:.2f}")
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
return None
except TypeError as e:
raise ValueError(f"Arguments must be numbers: {e}") from e
else:
return result
finally:
pass # cleanup always runs
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # None
try:
raise InsufficientFundsError(50.0, 100.0)
except InsufficientFundsError as e:
print(e) # Need 100.00 but only have 50.00Iterables, Iterators, and the Iterator Protocol
Python's `for` loop works on any *iterable* — an object that implements `__iter__()` returning an *iterator*. An iterator implements `__next__()` raising `StopIteration` when exhausted. Built-ins like `list`, `dict`, `str`, `range`, files, and generators are all iterables. `iter(obj)` and `next(obj)` are the protocol functions. Understanding this protocol explains how `for`, comprehensions, `zip`, `map`, `filter`, and `itertools` all interoperate.
class CountUp:
"""Iterator that counts from start to stop."""
def __init__(self, start: int, stop: int):
self._current = start
self._stop = stop
def __iter__(self):
return self # Iterator is its own iterable
def __next__(self):
if self._current >= self._stop:
raise StopIteration
val = self._current
self._current += 1
return val
for n in CountUp(1, 6):
print(n, end=" ") # 1 2 3 4 5
# Equivalent generator expression
gen = (n for n in range(1, 6))
print(list(gen)) # [1, 2, 3, 4, 5]Type Hints (PEP 484)
Python 3.5 introduced type hints via PEP 484. They are not enforced at runtime — they're metadata for static type checkers (`mypy`, `pyright`, `ruff`). The `typing` module provides `List`, `Dict`, `Optional`, `Union`, `Callable`, `TypeVar`, `Generic`, `Protocol`. Python 3.10+ replaced `Optional[X]` with `X | None` and `Union[X, Y]` with `X | Y`. Python 3.12 added `type` statement for type aliases.
from typing import TypeVar, Generic, Callable
from collections.abc import Iterable
T = TypeVar("T")
U = TypeVar("U")
def first(items: Iterable[T]) -> T | None:
return next(iter(items), None)
def transform(items: list[T], fn: Callable[[T], U]) -> list[U]:
return [fn(item) for item in items]
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # 2
print(first([10, 20, 30])) # 10
print(transform(["a", "bb", "ccc"], len)) # [1, 2, 3]Walrus Operator `:=` (Python 3.8+)
The walrus operator (`:=`, PEP 572) assigns a value to a variable and returns it in a single expression. It's most useful inside `while` conditions (avoiding a duplicate read), comprehension conditions (capturing an intermediate computed value), and `if` statements (avoiding re-computation). It makes imperative "compute, check, use" patterns more concise without sacrificing readability when used judiciously.
import re
# Walrus in a while loop — read chunks until EOF
def count_chars(text: str, chunk_size: int = 4) -> int:
total = 0
pos = 0
while chunk := text[pos:pos + chunk_size]:
total += len(chunk)
pos += chunk_size
return total
print(count_chars("Hello, World!")) # 13
# Walrus in comprehension — avoid recomputing expensive fn
data = [1, -2, 3, -4, 5]
results = [y for x in data if (y := x * x) > 4]
print(results) # [4, 9, 16, 25] — wait, condition is y > 4
# => [9, 16, 25]
# Walrus in regex match
text = "Order #12345 placed"
if m := re.search(r"#(\d+)", text):
print(f"Order ID: {m.group(1)}") # Order ID: 12345Structural Pattern Matching (`match`/`case`, Python 3.10+)
`match`/`case` (PEP 634) is not a simple switch — it performs structural decomposition. Patterns can match literals, variable capture (`case Point(x, y):`), sequences (`case [first, *rest]:`), mappings (`case {"action": action}:`), class patterns, OR patterns (`|`), and guards (`if condition`). Captured variables are bound in the case body. The `_` wildcard matches anything without binding.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def classify_point(p):
match p:
case Point(x=0, y=0):
return "origin"
case Point(x=0, y=y):
return f"on y-axis at {y}"
case Point(x=x, y=0):
return f"on x-axis at {x}"
case Point(x=x, y=y) if x == y:
return f"on diagonal at {x}"
case Point(x=x, y=y):
return f"point ({x}, {y})"
for pt in [Point(0, 0), Point(0, 5), Point(3, 0), Point(4, 4), Point(1, 2)]:
print(classify_point(pt))Dataclasses (Python 3.7+)
`@dataclass` auto-generates `__init__`, `__repr__`, `__eq__` (and optionally `__lt__`, `__hash__`, `__post_init__`) from class-level annotations. With `frozen=True` the class is immutable (and hashable). `field()` controls defaults, whether a field appears in `__repr__`, and `default_factory` for mutable defaults. Python 3.10 added `slots=True` for automatic `__slots__` (lower memory, faster attribute access).
from dataclasses import dataclass, field
from typing import ClassVar
@dataclass(order=True, frozen=True)
class Version:
major: int
minor: int
patch: int = 0
_registry: ClassVar[list] = [] # ClassVar excluded from __init__
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}"
@dataclass
class Package:
name: str
version: Version
dependencies: list[str] = field(default_factory=list)
_internal: str = field(default="", repr=False)
v1 = Version(3, 11)
v2 = Version(3, 12)
print(v1 < v2) # True (order=True generates __lt__)
print(str(v1)) # 3.11.0
pkg = Package("requests", v2, ["certifi", "urllib3"])
print(pkg)
# Package(name='requests', version=3.12.0, dependencies=['certifi', 'urllib3'])f-Strings and String Formatting
f-strings (PEP 498, Python 3.6+) embed expressions in string literals: `f"{expr}"`. They call `__format__` on the object with an optional format spec after `:`. Python 3.12 removed the restriction that f-string expressions couldn't contain the same quote type as the outer string. `=` suffix (`f"{value=}"`) prints both the expression text and its value — a debug shorthand.
import math
from datetime import datetime
# Format spec: [[fill]align][sign][#][0][width][grouping][.prec][type]
pi = math.pi
salary = 123456.789
now = datetime.now()
print(f"{pi:.4f}") # 3.1416
print(f"{salary:>15,.2f}") # 123,456.79 (right-aligned, 15 wide)
print(f"{42:#010b}") # 0b00101010 (binary with prefix, 10 wide)
print(f"{now:%Y-%m-%d}") # 2024-01-15 (date format)
# Debug shorthand (Python 3.8+)
x = [1, 2, 3]
print(f"{x=}") # x=[1, 2, 3]
# Nested f-strings and Python 3.12 same-quote expressions
items = ["a", "b", "c"]
print(f"{'|'.join(items)}") # a|b|c (Python 3.12+: same quote inside)