Expressions
An expression is anything Python can evaluate down to a value — a single literal, a chain of operators, a function call, an attribute lookup, or a comprehension. This topic ties operands, operators, and Python's expression-building syntax together.
An expression is a piece of code that Python evaluates to produce a value. Expressions are built from operands (literals, names, or other expressions) combined by operators, function calls, attribute access, indexing, slicing, and comprehensions. This topic surveys how expressions are constructed and evaluated, and distinguishes expressions (which produce values) from statements (which perform actions, and generally do not).
What Is an Expression?
An expression is any combination of values, names, operators, and calls that Python can reduce to a single resulting value. The simplest expressions are literals (42, "hi") and names (x); more complex expressions combine these with operators, calls, or other syntax. Crucially, an expression can always appear where a value is expected — as a function argument, the right-hand side of an assignment, inside another expression, and so on.
x = 5 # 5 is a literal expression
y = x + 3 # x + 3 is a compound expression
z = (x + 3) * 2 - 1 # expressions can nest arbitrarily
print(z) # 11Expressions vs Statements
Every expression produces a value; a statement performs an action and generally does not itself have a value. x = 5 is a statement (assignment) — it cannot be nested inside another expression like print(x = 5). But x + 5 is an expression and can be used anywhere a value is needed.
The assignment expression := (Python 3.8+) is a special, deliberate exception: it lets you assign as part of a larger expression, which the classic = statement cannot do.
# print(x = 5) # SyntaxError -- assignment is a statement, not an expression
print(x := 5) # OK -- the WALRUS operator IS an expression, introduced in Python 3.8
if (n := len([1, 2, 3])) > 2:
print(f"list has {n} items")Operands and Operators
An operand is a value an operator acts on. Operators combine one operand (unary, like -x or not x), two operands (binary, like a + b), or occasionally more, as in a chained comparison. Operators themselves are covered in depth in the dedicated Operators topic; here the focus is how they compose into larger expressions.
a, b, c = 2, 3, 4
result = a + b * c # b*c is evaluated first (higher precedence), then a + (b*c)
print(result) # 14
negated = -a # unary operator, one operand
print(negated) # -2Function Calls as Expressions
A function call — name(arguments) — is itself an expression: it evaluates to whatever the function returns (or None if the function has no explicit return). Because it's an expression, a call can be nested directly inside another expression.
def square(n):
return n * n
result = square(3) + square(4) # two nested call expressions, then addition
print(result) # 25
print(len(str(12345))) # nested calls: str() then len()Attribute Access
obj.attr is an expression that looks up the attribute named attr on obj, evaluating to whatever value is found (following the object's attribute lookup rules). Attribute access can be chained and combined with calls.
text = " Hello World "
cleaned = text.strip().lower().replace("world", "python")
print(cleaned) # 'hello python' -- each .method() call is itself an expressionIndexing and Slicing
obj[index] (indexing) and obj[start:stop:step] (slicing) are expressions that retrieve an element or a sub-sequence, dispatching to the object's __getitem__. Indexing returns a single element; slicing returns a new object of the same general kind (a sub-list, a substring, etc.).
Full slicing syntax and semantics (negative indices, step, out-of-range handling) are covered in the dedicated Slicing topic; here the point is only that both forms are expressions producing a value.
data = [10, 20, 30, 40, 50]
print(data[2]) # 30 -- single-element indexing expression
print(data[1:4]) # [20, 30, 40] -- slicing expression
print(data[::-1]) # [50, 40, 30, 20, 10] -- reversed sliceComprehensions as Expressions
A list/set/dict comprehension is a single expression that builds a new collection, evaluated eagerly (all at once) when it runs. Comprehensions combine a for clause, optional if filters, and an expression describing each output element — all inside one expression.
squares = [n * n for n in range(6)] # list comprehension
evens = {n for n in range(10) if n % 2 == 0} # set comprehension
square_map = {n: n * n for n in range(5)} # dict comprehension
print(squares, evens, square_map)Generator Expressions
A generator expression looks like a list comprehension but uses parentheses instead of brackets, and it evaluates lazily: it produces an iterator that computes each value on demand rather than building the entire collection up front. This makes it far more memory-efficient for large or infinite sequences.
Full generator mechanics (yield, laziness, exhaustion) are covered in the dedicated Generators topic; here the focus is that a generator expression is itself a single lazily-evaluated expression.
squares_gen = (n * n for n in range(1_000_000)) # no computation happens yet
print(type(squares_gen)) # <class 'generator'>
print(next(squares_gen), next(squares_gen)) # 0 1 -- computed one at a time
total = sum(n * n for n in range(6)) # generator expression passed directly to sum()
print(total) # 55Evaluation Order and Side Effects
Python evaluates expressions left to right, and subexpressions are generally evaluated in the order they're written, though operator precedence controls which operations group together first (see Operator Precedence & Evaluation for the full rules). Some expressions have side effects — they modify state as a byproduct of being evaluated, beyond just producing a value — which makes evaluation order matter.
Relying heavily on side effects buried inside expressions makes code harder to read and debug; prefer keeping expressions pure (side-effect-free) where practical, and use separate statements for actions that modify state.
log = []
def record(label, value):
log.append(label)
return value
result = record("a", 1) + record("b", 2) * record("c", 3)
print(result) # 7
print(log) # ['a', 'b', 'c'] -- evaluated left to right despite * binding tighter than +Key Takeaways
An expression is anything Python evaluates to a value; a statement performs an action and generally has no value of its own (the walrus operator := is a deliberate exception, letting assignment happen inside an expression).
Operands (literals, names, other expressions) combine via operators to form larger expressions.
Function calls, attribute access (obj.attr), indexing (obj[i]), and slicing (obj[a:b]) are all expressions — each evaluates to a value and can be nested inside other expressions.
List/set/dict comprehensions are single expressions evaluated eagerly; generator expressions look similar but evaluate lazily, producing values on demand.
Expressions are evaluated left to right, subject to operator precedence for grouping; expressions with side effects should be used sparingly, since evaluation order then becomes meaningfully observable.
Plain assignment (=) is a statement and cannot appear inside another expression; Python 3.8+'s walrus operator (:=) is specifically an assignment EXPRESSION for this purpose.
squares = (n * n for n in range(1_000_000)) print(len(squares)) # TypeError -- generators have no len() squares = [n * n for n in range(1_000_000)] # use a list comprehension if you need len() or repeated iteration print(len(squares))
A generator expression produces a lazy iterator with no known length and no re-iteration; use a list (or another concrete container) comprehension when you need len(), indexing, or to iterate more than once.
What distinguishes an expression from a statement in Python?
An expression evaluates to a value and can be nested inside other expressions; a statement performs an action and generally has no value of its own (aside from the deliberate exception of the walrus operator).
Is a function call an expression? What does it evaluate to if the function has no return statement?
Yes; a function call is an expression that evaluates to whatever the function returns, or to None if there is no explicit return statement.
What is the key evaluation-timing difference between a list comprehension and a generator expression?
A list comprehension evaluates eagerly, building the entire list immediately; a generator expression evaluates lazily, producing each value on demand as it's iterated.
Why can't you write print(x = 5) to both assign and print x in one line?
Because = is a statement, not an expression, so it cannot appear as a function argument; you would use the walrus operator := instead, e.g. print(x := 5).
What do obj[i] and obj[a:b] both dispatch to under the hood?
Both are expressions that call the object's __getitem__ method -- indexing with a single index, slicing with a slice object.
In a() + b() * c(), does * having higher precedence than + change the ORDER in which a(), b(), and c() are called?
No -- Python still evaluates the calls left to right (a(), then b(), then c()); precedence only affects which operation's RESULT is combined first (b()'s and c()'s results are multiplied before adding a()'s result), not the call order.
Why is it good practice to avoid side effects buried inside expressions?
Because expressions with side effects tie correctness to evaluation order, making code harder to read, reason about, and debug; separating actions into their own statements makes the sequence of effects explicit.
Write a single expression (a generator expression passed to sum()) that computes the sum of squares of even numbers from 0 to 9.
result = sum(n * n for n in range(10) if n % 2 == 0) print(result) # 0 + 4 + 16 + 36 + 64 = 120
A generator expression with a filtering if clause, consumed directly by sum() without ever materializing an intermediate list.
What does this code print, and what kinds of expressions are combined to produce it?
5 -- data["a"] (indexing) then [-1] (indexing again) gives 3; len(data["b"]) (a function call on an indexing expression) gives 2; 3 + 2 = 5. It chains indexing, attribute-free lookups, and a function call, all as nested expressions.
Write code using the walrus operator to read values from a list, printing each value along with a running total, stopping once the total exceeds 10.
numbers = [3, 4, 2, 5, 6, 1] total = 0 for n in numbers: if (total := total + n) > 10: print(f"stopped at {n}, total={total}") break print(f"n={n}, total={total}")
The walrus operator (total := total + n) both updates and yields the new total within the if condition itself, letting the check and the update happen in one expression.
data = {"a": [1, 2, 3], "b": [4, 5]}
result = data["a"][-1] + len(data["b"])
print(result)