Cloud Tech by Victor
BackendBeginner

Python Syntax Fundamentals

How Python's significant whitespace, dynamic typing, and object model shape idiomatic code, and the mutable-default-argument trap that catches almost everyone once.

Updated 2026-07-225 min read

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

ConceptExampleNote
Variable assignmentx = 5No type declaration; type is inferred at runtime
Function definitiondef f(x, y=10): ...Default values evaluated once, at definition time
f-stringf"{name} is {age}"Preferred string formatting since Python 3.6
Truthinessif items:Empty collections/0/None/"" are falsy
Type hintsdef 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.

python
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 variable

Syntax

python
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

python
# 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 items

Type 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 against None instead of is None, works in practice for None specifically 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 for loop with .append(), because the looping happens in optimized C code rather than interpreted bytecode per iteration.

Best Practices

  • Default mutable arguments to None and 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 not for identity comparisons (especially against None) and ==/!= for value comparisons; the two are not interchangeable in general, even though they often produce the same result for simple cases.

Interview questions

Why does Python use indentation instead of braces to define code blocks, and what problem does this create?

Python uses indentation as syntax specifically to force a consistent visual structure, code that looks nested is nested, with no possibility of a brace mismatch making the visual and actual structure disagree, a real class of bugs in brace-delimited languages. The trade-off is that whitespace becomes semantically meaningful: mixing tabs and spaces, or an accidentally misaligned line, is a syntax error (or worse, silently changes which block a line belongs to) rather than a cosmetic issue. Python 3 disallows mixing tabs and spaces in the same file specifically to prevent the silent version of this problem.

Why is using a mutable object (like a list) as a default argument value a common bug in Python?

Default argument values are evaluated exactly once, when the function is defined, not on every call, so a mutable default like `def f(items=[])` creates one list object that is shared across every call that doesn't explicitly pass its own `items`. Appending to it in one call leaves those items present the next time the function is called with the default, which looks like inexplicable state leaking between unrelated calls. The fix is defaulting to `None` and creating a new list inside the function body when `items is None`, so every call that needs the default gets its own fresh object.

What is the difference between `is` and `==` in Python?

`==` calls the object's `__eq__` method and checks value equality, whether two objects represent the same value, even if they are different objects in memory. `is` checks identity, whether two names refer to the exact same object in memory. Two separate list literals with identical contents are `==` but not `is`, because they're distinct objects with equal values. `is` is correct for comparing against singletons like `None` (`x is None`, not `x == None`), since there is exactly one `None` object and identity is the more precise, idiomatic check.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement