Overview
Python's core design choices, significant whitespace, dynamic typing, and "everything is an object", trade some of the explicitness of statically typed, brace-delimited languages for readability and fast iteration. That trade-off comes with its own sharp edges: whitespace being syntactically meaningful means indentation mistakes are real bugs, not style issues, and dynamic typing means many classes of errors that a compiler would catch elsewhere only surface at runtime. Understanding the object model, that even integers and functions are objects with identity, explains why is and == behave differently and why default arguments have their well-known mutable-object trap.
Quick Reference
| Concept | Example | Note |
|---|---|---|
| Variable assignment | x = 5 | No type declaration; type is inferred at runtime |
| Function definition | def f(x, y=10): ... | Default values evaluated once, at definition time |
| f-string | f"{name} is {age}" | Preferred string formatting since Python 3.6 |
| Truthiness | if items: | Empty collections/0/None/"" are falsy |
| Type hints | def f(x: int) -> str: | Optional, not enforced at runtime |
The groups below aren't an exhaustive language reference (see the official Python tutorial and built-in types documentation for that), but the everyday syntax that covers almost all beginner-to-intermediate Python code.
x = 5 # int
y = 3.14 # float
name = "Ada" # str
is_valid = True # bool
nothing = None # NoneType
a, b = 1, 2 # multiple assignment
a, b = b, a # swap without a temporary variablenums = [1, 2, 3]
nums.append(4) # [1, 2, 3, 4]
nums[0] # 1 (first item)
nums[-1] # 4 (last item)
nums[1:3] # [2, 3] (slice)
squares = [n * n for n in nums] # list comprehensionperson = {"name": "Ada", "age": 30}
person["age"] # 30
person.get("email", "n/a") # "n/a" - no KeyError if missing
person["email"] = "ada@example.com"
unique = {1, 2, 2, 3} # {1, 2, 3} - duplicates dropped
unique.add(4)for n in [1, 2, 3]:
print(n)
for i, n in enumerate(["a", "b"]):
print(i, n) # 0 a, then 1 b
n = 0
while n < 3:
n += 1def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
def total(*args, **kwargs):
return sum(args)
square = lambda x: x * xclass User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hi, {self.name}"
class Admin(User):
def greet(self):
return f"{super().greet()} (admin)"try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Can't divide by zero: {e}")
finally:
print("Always runs")
raise ValueError("Invalid input")Syntax
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"Examples
# The mutable default argument trap - items is shared across calls
def add_item(item, items=[]):
items.append(item)
return items
add_item("a") # ['a']
add_item("b") # ['a', 'b'] - surprising! Same list object every call.
# Fixed version
def add_item_safe(item, items=None):
if items is None:
items = []
items.append(item)
return itemsType hints are not enforced at runtime
def f(x: int) -> str documents intent and enables static checkers (mypy, pyright) to catch mismatches before running the code, but Python itself never checks or enforces these annotations at runtime, passing a string where int is hinted will not raise an error on its own.
Visual Diagram
Common Mistakes
- Using a mutable object (list, dict) as a default argument value, causing state to leak between unrelated calls.
- Using
==to compare againstNoneinstead ofis None, works in practice forNonespecifically but is the wrong idiom and can mask bugs with custom__eq__implementations. - Mixing tabs and spaces for indentation, which Python 3 treats as a syntax error rather than silently picking one.
- Relying on type hints as if they were enforced; they help tooling and readers, not the runtime, unless explicitly validated (e.g., with Pydantic or a runtime type-checking library).
Performance
- Python's dynamic typing means every attribute access and method call involves a runtime lookup, which is inherently slower than statically dispatched languages; this is a language-level trade-off, not something idiomatic code can opt out of.
- String concatenation in a loop (
s += x) creates a new string object each time since strings are immutable, using''.join(...)on a list is significantly faster for building large strings. - List comprehensions are generally faster than the equivalent
forloop with.append(), because the looping happens in optimized C code rather than interpreted bytecode per iteration.
Best Practices
- Default mutable arguments to
Noneand create the actual mutable object inside the function body. - Use f-strings for string formatting; they're both the most readable and generally the fastest option in modern Python.
- Add type hints even though they're unenforced; they meaningfully improve editor support and catch real bugs when paired with a static checker in CI.
- Use
is/is notfor identity comparisons (especially againstNone) and==/!=for value comparisons; the two are not interchangeable in general, even though they often produce the same result for simple cases.