<< Home

Chapter 4: Operators and Expressions

Operators are the symbols that let Python combine values, compare objects, and control logic. Together with expressions, they form the language’s working grammar—the way ideas are turned into action. Understanding how operators behave is key to writing code that is both clear and efficient.

Every operation in Python produces a value, even those that seem to exist only for their effect. Expressions combine values and operators to compute results, while statements often use those results to drive control flow or perform actions. This blend of simplicity and precision is what makes Python both expressive and readable.

Python’s operators are grouped by purpose: arithmetic, comparison, logical, bitwise, membership, and identity. Each follows predictable rules and precedences, meaning that once you learn the pattern for one category, the rest fall naturally into place. Parentheses can always be used to make the intended order explicit.

Unlike many languages, Python’s operators work with a wide range of object types. The same symbol can mean numeric addition for numbers, concatenation for strings, or merging for lists. This flexibility comes from special methods like __add__() and __eq__(), which define how objects respond to operators.

This chapter explores how expressions are built, how precedence and associativity control evaluation order, and how Python’s operators provide a compact yet expressive vocabulary for computation, comparison, and logic. Mastering them will help you write code that communicates clearly—both to the interpreter and to other readers.

Arithmetic, comparison, and logical operators

Operators are at the heart of expression syntax in Python. They define how values combine, compare, and interact. The most common groups are arithmetic operators (for mathematical computation), comparison operators (for testing relationships), and logical operators (for combining conditions). Each group has clear, predictable behaviour that applies across many object types.

Arithmetic operators

Arithmetic operators perform mathematical operations on numeric types and, where appropriate, on sequences like strings or lists. Python uses familiar symbols for these operations:

a = 10
b = 3

print(a + b)   # 13
print(a - b)   # 7
print(a * b)   # 30
print(a / b)   # 3.333...
print(a // b)  # 3
print(a % b)   # 1
print(a ** b)  # 1000

Arithmetic operators can also act on compatible sequence types. For instance, "a" * 3 repeats a string, and [1, 2] + [3] concatenates lists. These operations follow the same symbolic pattern as numeric arithmetic but apply meaningfully to collections.

⚠️ The / operator always produces a float, even if both operands are integers. Use // when you need integer division that discards the remainder.

Comparison operators

Comparison operators evaluate relationships between two values, producing True or False. They are essential for control flow and condition testing:

x = 5
y = 8

print(x == y)  # False
print(x != y)  # True
print(x < y)   # True

Comparisons can be chained for readability, following standard mathematical style:

age = 25
print(18 <= age < 30)  # True

Python evaluates such chains without repeating the variable, and stops as soon as a comparison fails. This keeps conditionals concise and expressive.

Logical operators

Logical operators combine or invert Boolean expressions. They control the flow of logic in conditionals and loops:

logged_in = True
is_admin = False

if logged_in and not is_admin:
  print("User access granted")

Unlike purely Boolean operators in other languages, Python’s and and or return one of their operands rather than a strict Boolean value. This behaviour supports common idioms:

name = "" or "Anonymous" # evaluates to "Anonymous"
status = "active" and 1  # evaluates to 1
💡 Logical expressions are evaluated left to right with short-circuiting. If the first part of an and expression is false, or the first part of an or expression is true, Python skips the rest because the outcome is already determined.

Arithmetic, comparison, and logical operators together define how Python computes, tests, and decides. They form the essential vocabulary for writing expressions that describe both numerical and logical relationships within a program.

Identity and membership

Beyond arithmetic and logic, Python provides two small but powerful families of operators that express relationships of a different kind: identity and membership. They do not compare values in the usual sense—they ask whether something is a specific object, or whether it belongs to a collection.

Identity operators

Identity operators test whether two names refer to the same object in memory. They are especially important when dealing with mutable objects or special singletons such as None. The operators are:

a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(a is b) # True (same object)
print(a is c) # False (different object, same content)
print(a == c) # True (values are equal)

The difference between is and == is subtle but essential. The == operator compares the values of two objects, while is checks whether they are literally the same object in memory. Most of the time, equality is what you want, but is is correct when testing for None or other singletons.

💡 Use is None and is not None for null checks. This is both faster and clearer than equality comparison, since there is only one None object in any Python process.

Membership operators

Membership operators check whether an element exists within a sequence, set, or mapping. They read naturally, almost like plain English, and return True or False depending on the presence of the item.

letters = ["a", "b", "c"]

print("a" in letters)      # True
print("z" not in letters)  # True

For dictionaries, the membership test applies to keys, not values. To check whether a value exists, use the .values() method explicitly.

person = {"name": "Ada", "age": 36}
print("name" in person)          # True  (key check)
print("Ada" in person.values())  # True  (value check)

Membership testing is highly efficient for sets and dictionaries because these types use hash tables for storage. For lists, tuples, and strings, the operation scans sequentially, so its cost grows with size.

⚠️ Avoid confusing identity with equality. Two distinct objects can be equal in value but not identical. Likewise, in checks membership, not identity. It asks “is this element contained here?”, not “is this the same object?”.

Together, identity and membership complete Python’s expressive set of relational operators. They let code describe relationships between objects and collections with precision that reads as naturally as English, which is one of the hallmarks of Python’s design philosophy.

Bitwise operators

Bitwise operators act directly on the binary representation of integers. Instead of working with whole numbers as quantities, they manipulate the individual bits that make those numbers up. These operators are common in low-level programming, networking, graphics, and performance-critical code where compact control of data is important.

⚠️ Bitwise operations only work on integers. Attempting them on floats, strings, or other objects raises a TypeError. Always ensure operands are integers or explicitly convert them before use.

Each integer in Python is stored in binary form, using ones and zeros. Bitwise operators perform logical operations on those bits position by position:

a = 6    # binary 110
b = 3    # binary 011

print(a & b)   # 2  (010)
print(a | b)   # 7  (111)
print(a ^ b)   # 5  (101)
print(~a)      # -7 (bitwise inversion)
print(a << 1)  # 12 (shift left)
print(a >> 1)  # 3  (shift right)

Left and right shifts effectively multiply or divide integers by powers of two, which can be useful in optimisation or when working with binary data streams. For example, n << 3 is the same as n * 8. Bitwise operations can be visualised more easily by converting numbers to binary strings using bin():

print(bin(6))      # 0b110
print(bin(3))      # 0b11
print(bin(6 & 3))  # 0b10

Although Python integers can grow arbitrarily large, bitwise operators still behave as if working on an infinite string of bits in two’s complement form. This explains why ~6 becomes -7: the inversion flips all bits and changes the sign.

💡 Bitwise operators are often combined with masks; specific binary patterns used to extract or modify certain bits. For instance, value & 0xFF isolates the lowest 8 bits of value.

Even though bitwise logic may seem niche in everyday Python programming, it reveals the level beneath the surface—the raw binary patterns that all computation rests on. Knowing how these operators behave gives you precise control when you need it, and a deeper understanding of how Python represents and manipulates data at its most fundamental level.

Assignment expressions

Until Python 3.8, assignment was always a statement, not an expression. That meant you could not assign a value as part of another expression. You had to do it in a separate line. The assignment expression operator, written as := and often called the walrus operator, changes this. It allows you to assign a value to a name as part of a larger expression, returning that value at the same time. This makes certain patterns more compact and expressive, especially in loops and conditionals where a value needs to be both tested and reused.

# without the walrus operator
line = input("Enter text: ")
while line != "":
  print("You said:", line)
  line = input("Enter text: ")

# with the walrus operator
while (line := input("Enter text: ")) != "":
  print("You said:", line)
💡 The walrus operator never creates a new variable scope. It binds the name in the same scope as a normal assignment, so it follows the usual rules of locals, globals, and closures.

Here, the input is both captured and tested in a single expression. The operator assigns the result of input() to line and also returns that result for comparison. The parentheses are required because the operator has lower precedence than most comparisons and arithmetic operations.

Assignment expressions are most useful when a value would otherwise need to be computed twice, or when combining assignment and condition logic improves clarity. They should be used sparingly and only when the result reads naturally.

if (n := len(items)) > 0:
  print(f"{n} items found.")
⚠️ Avoid overusing assignment expressions in complex conditions. If readability suffers or the expression becomes hard to scan, split it into separate statements instead. Python’s clarity principle still applies: explicit is better than implicit.

Assignment expressions make Python slightly more expressive without compromising its simplicity. They enable concise patterns where a value is both needed and tested, bridging the small gap between statements and expressions in the language’s design.

Operator precedence and associativity

When an expression contains several operators, Python must decide which parts to evaluate first. This order is called operator precedence. Operators with higher precedence bind more tightly, meaning they are evaluated before those with lower precedence. When operators share the same precedence level, associativity determines whether evaluation proceeds from left to right or right to left.

Parentheses can always be used to make order explicit, and doing so is often best for clarity. But knowing the general precedence rules helps you read expressions with confidence and understand why Python produces a given result. The following list shows Python’s operator precedence from highest to lowest. Operators on the same line share the same level and are evaluated according to their associativity.

Highest to lowest precedence

OperatorDescription
()Grouping or function calls
x[index], x(attr)Subscripts, slicing, attribute access
**Exponentiation (right-associative)
+x, -x, ~xUnary plus, minus, bitwise NOT
*, /, //, %Multiplication, division, floor division, modulus
+, -Addition, subtraction
<<, >>Bitwise shifts
&Bitwise AND
^Bitwise XOR
|Bitwise OR
in, not in, is, is not, <, <=, >, >=, ==, !=Comparisons, membership, identity
notLogical NOT
andLogical AND
orLogical OR
if–elseConditional expressions
:=Assignment expression (walrus)
=, +=, -=, *=, /=, //=, %=, **=, &=, |=, ^=, <<=, >>=Assignment operators (right-associative)
yield, yield fromYield expressions
return, lambdaReturn, lambda expressions
💡 When in doubt, use parentheses. They not only guarantee correct evaluation but also make intent clear to readers, even when operator precedence would handle it correctly.

Associativity determines how operators of equal precedence are grouped. Most Python operators are left-associative, meaning evaluation proceeds from left to right. Only exponentiation (**), assignment operators, and the walrus operator (:=) associate from right to left:

print(2 ** 3 ** 2)  # 2 ** (3 ** 2) = 512
⚠️ Long or complex expressions can quickly become hard to read, even if precedence is correct. Breaking them into intermediate variables often makes code clearer and easier to debug.

This precedence hierarchy defines how all of Python’s operators fit together. Understanding it turns what might look like a dense expression into a predictable and readable sequence of operations. In practice, parentheses and good naming will always keep your intent explicit, which is the real goal of Pythonic style.