March 28, 2025
Python Beginner Operators Expressions

Python Operators, Expressions, and Truthiness

If you think of variables as nouns and control flow as verbs, then operators are the adverbs of programming. They're how we modify, compare, and combine values into meaningful expressions. Without them, Python would just be a filing system for storing numbers and strings. With them? We can write logic that decides, transforms, and validates data in ways that make your programs actually do something.

In this article, we're going deep on operators, the arithmetic ones that do math, the comparison ones that ask yes/no questions, the logical ones that combine answers, and the bitwise ones that work at the binary level. We'll also tackle truthiness: the often-confusing concept of what Python considers "true" and "false" when you're not explicitly comparing things. And yes, we'll cover the walrus operator :=, that sleek Python 3.8+ addition that lets you assign and check in one smooth move.

By the end, you'll know not just how operators work, but when to use them, what pitfalls to watch for, and how to leverage short-circuit evaluation to write cleaner, faster code.

Table of Contents
  1. What Is an Expression, Anyway?
  2. Arithmetic Operators: More Than Just + and -
  3. Comparison Operators: Asking Questions
  4. Comparison Chaining: One of Python's Superpowers
  5. Logical Operators: and, or, not
  6. The `and` Operator
  7. The `or` Operator
  8. The `not` Operator
  9. Bitwise Operators: Working with Binary
  10. The Walrus Operator: Assignment Expressions (Python 3.8+)
  11. While Loop Example
  12. List Comprehension Example
  13. Operator Precedence Demystified
  14. Truthiness: Python's Hidden Logic
  15. The Truthiness Table
  16. Leveraging Truthiness in Your Code
  17. The "Truthy" Pitfall: Unexpected Behavior
  18. Short-Circuit Evaluation: Writing Efficient Code
  19. Common Expression Mistakes
  20. Identity Operators: is vs ==
  21. Putting It All Together: A Real Example
  22. Summary and Next Steps

What Is an Expression, Anyway?

Before we dive into individual operators, it's worth stepping back and asking a deceptively simple question: what exactly is a Python expression? An expression is any piece of code that Python can evaluate to produce a value. That sounds abstract, so let's make it concrete. The literal 42 is an expression, it evaluates to the integer 42. The variable reference x is an expression, it evaluates to whatever value x holds. And x + 42 * y is an expression too, Python evaluates it using operator rules and produces a single result.

The distinction matters because not everything in Python is an expression. Statements like import os, if x > 0:, or def my_func(): don't produce values, they perform actions. Expressions, by contrast, always resolve to something you can store, print, or pass into another function. This is why print(2 + 2) works: the expression 2 + 2 evaluates to 4 first, and then print receives that value. You can even nest expressions arbitrarily deep, combining arithmetic, comparisons, function calls, and logical operators into a single compound expression that Python unravels step by step.

Understanding expressions is foundational to understanding operators, because operators are the glue that lets you build complex expressions from simpler ones. Every time you write score >= passing_grade and attempts < max_attempts, you're constructing an expression out of comparison and logical operators. Python evaluates the comparisons first, gets two boolean values, then applies and to produce a final result. The whole chain is one expression, and it evaluates to exactly one value. Once that mental model clicks, expressions are things that produce values, operators are the combinators, the rest of this article will feel less like memorizing rules and more like learning a grammar.

Arithmetic Operators: More Than Just + and -

You know about addition and subtraction. But Python's arithmetic operators have some surprises, especially around division.

Before you look at the output, notice that we're using f-strings to label each result inline. That's the style we'll carry through the whole article, it keeps the connection between the expression and its result immediately obvious.

python
# Basic arithmetic operators
a = 10
b = 3
 
print(f"Addition: {a + b}")           # 13
print(f"Subtraction: {a - b}")        # 7
print(f"Multiplication: {a * b}")     # 30
print(f"True division: {a / b}")      # 3.3333333333333335
print(f"Floor division: {a // b}")    # 3
print(f"Modulo: {a % b}")             # 1
print(f"Exponentiation: {a ** 2}")    # 100

Output:

Addition: 13
Subtraction: 7
Multiplication: 30
True division: 3.3333333333333335
Floor division: 3
Modulo: 1
Exponentiation: 100

Here's the gotcha that catches beginners: true division (/) always returns a float, even when dividing evenly. Floor division (//) returns an integer, rounding down to the nearest whole number. If you're coming from Python 2, where / did integer division by default, this behavior flip was intentional, true division is more mathematically sensible. The float result from / means you can always trust its precision, while // is your tool when you specifically need whole-number quotients, like when you're calculating how many full pages fit in a document, or how many complete groups you can form from a set of items.

The modulo operator (%) returns the remainder. It's incredibly useful for checking divisibility, cycling through lists, and generating patterns. Anytime you need to know "does this number divide evenly?" or "which bucket does this item fall into?", modulo is your answer:

python
# Modulo for practical uses
for i in range(1, 11):
    if i % 2 == 0:
        print(f"{i} is even")
    else:
        print(f"{i} is odd")

Output:

1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
10 is even

The even/odd check is just the beginning, modulo also powers circular buffers, round-robin scheduling, and wrapping behavior in grid-based games. Any time you need "loop back to zero after reaching N," modulo is the operator you reach for.

Exponentiation (**) is straightforward but remember it has higher precedence than multiplication. 2 * 3 ** 2 is 2 * 9 = 18, not (2 * 3) ** 2 = 36. We'll dig into the full precedence hierarchy in a dedicated section below, but for now just keep that one surprise in mind.

Comparison Operators: Asking Questions

Comparison operators ask yes/no questions about data. They always return a boolean: True or False. Think of them as the interrogative form of your expressions, they take values on both sides and produce a judgment.

python
x = 5
y = 10
 
print(f"x == y: {x == y}")        # False
print(f"x != y: {x != y}")        # True
print(f"x < y: {x < y}")          # True
print(f"x <= y: {x <= y}")        # True
print(f"x > y: {x > y}")          # False
print(f"x >= y: {x >= y}")        # False

Output:

x == y: False
x != y: True
x < y: True
x <= y: True
x > y: False
x >= y: False

Notice that equality uses == (two equals signs), not =. This is one of the most common mistakes beginners make, and Python will raise a SyntaxError if you write if x = 5: instead of if x == 5:. The single = is reserved for assignment; the double == is for comparison. Burn that distinction into your muscle memory now and you'll save yourself hours of debugging later.

Comparison Chaining: One of Python's Superpowers

Here's something Python does beautifully that many languages don't: you can chain comparisons. Instead of writing x > 5 and x < 10, you write 5 < x < 10. It's more readable and actually more efficient because Python evaluates x only once. This mirrors how we naturally write mathematical inequalities on paper, which makes Python code closer to the way humans actually think about ranges.

python
# Comparison chaining
age = 25
 
# Readable, Pythonic
if 18 <= age < 65:
    print(f"Age {age} is in working range")
 
# Also works with multiple chains
score = 85
if 60 < score <= 100:
    print(f"Score {score} is passing")
 
# Chain as many as you want (though 2-3 is typical)
x = 5
if 0 < x <= 10 <= 20:
    print("x is between 0 and 10, and 10 is less than or equal to 20")

Output:

Age 25 is in working range
Score 85 is passing
x is between 0 and 10, and 10 is less than or equal to 20

The real beauty of chained comparisons is that they're not just syntactic sugar, Python genuinely evaluates them more efficiently. Each intermediate value is computed only once, and the chain short-circuits if any comparison fails. So 0 < x <= 10 <= 20 stops at the first failing link rather than evaluating everything.

Logical Operators: and, or, not

Logical operators combine or negate boolean values. But here's where Python gets clever: they don't just return True or False. They return the actual value of the expression that determined the result. This behavior, returning the decisive operand rather than a strict boolean, is what enables the expressive default-value patterns you'll see throughout Pythonic code.

The and Operator

and returns the first falsy value it encounters, or the last value if all are truthy. This is called short-circuit evaluation, Python stops evaluating as soon as it knows the answer. The key insight is that if and finds a falsy value early, it knows the whole expression must be falsy, so there's no point evaluating anything else.

python
# and operator behavior
result1 = 0 and 5
print(f"0 and 5: {result1}")                    # 0 (first falsy)
 
result2 = 5 and 10
print(f"5 and 10: {result2}")                   # 10 (last value)
 
result3 = 5 and 10 and 0 and 20
print(f"5 and 10 and 0 and 20: {result3}")     # 0 (first falsy)
 
# Short-circuit in action: expensive_function never runs
def expensive_function():
    print("This is expensive!")
    return True
 
if False and expensive_function():
    print("Won't reach here")
# expensive_function never executes!

Output:

0 and 5: 0
5 and 10: 10
5 and 10 and 0 and 20: 0

Notice that expensive_function() never printed anything because Python short-circuited. This is a performance win, if the first condition is False, why evaluate the rest? In real-world code, that "expensive function" might be a database query, a network call, or a complex calculation. Short-circuiting means you can place your cheap guard conditions first and let them protect the expensive ones from running unnecessarily.

The or Operator

or returns the first truthy value, or the last value if all are falsy. This mirrors and's logic: once or finds a truthy value, it knows the whole expression is truthy, so it stops right there and returns that value.

python
# or operator behavior
result1 = 0 or 5
print(f"0 or 5: {result1}")                    # 5 (first truthy)
 
result2 = 0 or False or 10
print(f"0 or False or 10: {result2}")          # 10 (first truthy)
 
result3 = 0 or False or None
print(f"0 or False or None: {result3}")        # None (all falsy, returns last)
 
# Practical: provide defaults
user_name = None
display_name = user_name or "Anonymous"
print(f"Display name: {display_name}")

Output:

0 or 5: 5
0 or False or 10: 10
0 or False or None: None
Display name: Anonymous

The or operator is a classic way to provide default values. If user_name is falsy (including None), use "Anonymous" instead. You'll see this pattern constantly in Python codebases, it's concise, readable, and idiomatic. Just keep in mind the truthiness caveat we'll cover later: this pattern breaks down if 0 or an empty string are valid non-default values you want to preserve.

The not Operator

not inverts a boolean. It always returns a strict True or False, not the value itself. Unlike and and or, which pass through the decisive operand, not always commits to a pure boolean result. This makes it useful when you specifically need a boolean, not just a truthy-or-falsy value.

python
# not operator
print(f"not True: {not True}")           # False
print(f"not False: {not False}")         # True
print(f"not 0: {not 0}")                 # True
print(f"not 5: {not 5}")                 # False
print(f"not []: {not []}")               # True
print(f"not [1, 2]: {not [1, 2]}")      # False

Output:

not True: False
not False: True
not 0: True
not 5: False
not []: True
not [1, 2]: False

The pattern if not items: is idiomatic Python for checking whether a list (or other collection) is empty. It reads naturally in English, "if not items", which is exactly the kind of readable code Python was designed to encourage.

Bitwise Operators: Working with Binary

Bitwise operators manipulate individual bits in integers. They're less common in everyday Python, but invaluable for systems programming, game development, and certain algorithms. To understand what they do, you need to think in binary: the number 5 is 0101 in binary, and 3 is 0011. Bitwise operators work on each pair of corresponding bits independently.

python
# Bitwise operators
a = 5      # Binary: 0101
b = 3      # Binary: 0011
 
print(f"a & b (AND): {a & b}")          # 1 (0001)
print(f"a | b (OR): {a | b}")           # 7 (0111)
print(f"a ^ b (XOR): {a ^ b}")          # 6 (0110)
print(f"~a (NOT): {~a}")                # -6 (inverts all bits)
print(f"a << 1 (left shift): {a << 1}") # 10 (shifts bits left, fills with 0)
print(f"a >> 1 (right shift): {a >> 1}")# 2 (shifts bits right)

Output:

a & b (AND): 1
a | b (OR): 7
a ^ b (XOR): 6
~a (NOT): -6
a << 1 (left shift): 10
a >> 1 (right shift): 2

Left shift (<<) is equivalent to multiplying by powers of 2, and right shift (>>) divides by powers of 2, these operations are extremely fast at the hardware level, which is why you see them in performance-critical code like cryptography and image processing.

Practical use cases:

  • Bit masking: Check if a specific bit is set (e.g., file permissions in Unix)
  • Power-of-two checks: n & (n - 1) == 0 checks if n is a power of 2
  • Swapping without temp variable: a, b = b, a (or use XOR: a ^= b; b ^= a; a ^= b)
python
# Check if a number is a power of 2
def is_power_of_two(n):
    return n > 0 and (n & (n - 1)) == 0
 
print(f"8 is power of 2: {is_power_of_two(8)}")      # True
print(f"10 is power of 2: {is_power_of_two(10)}")    # False
print(f"16 is power of 2: {is_power_of_two(16)}")    # True

Output:

8 is power of 2: True
10 is power of 2: False
16 is power of 2: True

The n & (n - 1) == 0 trick works because powers of 2 in binary look like 10000..., exactly one 1-bit followed by zeros. Subtracting 1 from them gives 01111..., and ANDing those two patterns together always yields zero. For any non-power-of-two, the patterns overlap and the AND produces a nonzero result. It's elegant, fast, and a great example of how bit manipulation rewards people who think in binary.

The Walrus Operator: Assignment Expressions (Python 3.8+)

The walrus operator := assigns a value and uses it in the same expression. It reads like "assignment as expression," and it elegantly solves the problem of "I need to check this value, but I also need to store it." The name comes from the operator's resemblance to a walrus face when rotated, the colon forms the eyes and the equals sign the tusks. More importantly, it lets you collapse a common two-step pattern (assign, then check) into a single concise expression.

While Loop Example

python
# Without walrus: awkward repeated assignment
data = [1, 2, 3, 4, 5]
index = 0
while index < len(data):
    value = data[index]
    print(f"Processing {value}")
    index += 1
 
# With walrus: cleaner, assignment happens inside the condition
lines = ["first", "second", "third"]
index = 0
while (line := lines[index] if index < len(lines) else None) is not None:
    print(f"Line: {line}")
    index += 1

Better yet, use walrus with iter() for file reading:

python
# Reading a file line by line (classic walrus use)
# This is pseudocode since we're not reading an actual file here
# In real usage:
# with open("myfile.txt") as f:
#     while (line := f.readline().strip()) != "":
#         print(f"Processing: {line}")
 
# Simpler example with a list
data = iter(["apple", "banana", "cherry", ""])
while (item := next(data, "")) != "":
    print(f"Item: {item}")

Output:

Item: apple
Item: banana
Item: cherry

The file-reading pattern is where walrus shines brightest. Without it, you'd either read the same line twice (once to check, once to use) or restructure your loop awkwardly. With walrus, the assignment and the check happen in the same expression, and the code flows exactly as you'd describe it in plain English: "while there's still a line to read, process it."

List Comprehension Example

The walrus operator shines in comprehensions where you need to reuse a computed value. Without it, you'd have to call the same expensive function twice, once in the filter condition and once in the output expression.

python
# Without walrus: recompute square each time
squared = [x * x for x in range(1, 6) if (x * x) > 10]
print(f"Squared (no walrus): {squared}")
 
# With walrus: compute once, reuse
squared_walrus = [y for x in range(1, 6) if (y := x * x) > 10]
print(f"Squared (with walrus): {squared_walrus}")
 
# More complex: filtering and transforming based on computed value
data = [1, 2, 3, 4, 5]
result = [(n, square) for n in data if (square := n * n) > 10]
print(f"Numbers and their squares (>10): {result}")

Output:

Squared (no walrus): [16, 25]
Squared (with walrus): [16, 25]
Numbers and their squares (>10): [(4, 16), (5, 25)]

The last example is particularly instructive: walrus lets you capture the computed square during filtering and reuse it in the output tuple. The alternative would be to either compute the square twice or write a full for-loop instead of a comprehension. Walrus gives you the clarity of a comprehension without the redundancy.

Operator Precedence Demystified

Python evaluates expressions in a specific order, and if you don't know that order, you'll write bugs that are maddeningly hard to diagnose. The classic example: 2 + 3 * 4 evaluates to 14, not 20, because multiplication happens before addition. This is exactly how standard mathematical notation works, and Python follows those conventions faithfully. But the full precedence table goes well beyond arithmetic, and the surprises lurk in how logical, comparison, and bitwise operators interact.

Here's a simplified precedence table (highest to lowest):

Operator(s)Name
()Parentheses
**Exponentiation
+x, -x, ~xUnary plus, minus, bitwise NOT
*, /, //, %Multiplication, division, floor division, modulo
+, -Addition, subtraction
<<, >>Bitwise shifts
&Bitwise AND
^Bitwise XOR
|Bitwise OR
==, !=, <, <=, >, >=, is, is not, in, not inComparisons
notLogical NOT
andLogical AND
orLogical OR
if elseConditional expression (ternary)
:=Walrus operator

The table might look intimidating, but a few rules of thumb carry you most of the way. Arithmetic operators are always higher precedence than comparisons, so a + b > c - d computes both sides of the arithmetic first, then compares. Comparisons are always higher precedence than logical operators, so x > 0 and y < 10 checks both comparisons first, then applies and. And not is higher precedence than and, which is higher than or. That last point means not a and b is (not a) and b, not not (a and b). Parentheses resolve all ambiguity instantly, and the golden rule is: when in doubt, add parentheses. Your future self will thank you.

python
# Confusing (relies on precedence knowledge)
result1 = 2 + 3 * 4 ** 2 - 1
 
# Clear (same answer, explicit)
result2 = 2 + (3 * (4 ** 2)) - 1
 
print(f"Confusing: {result1}")
print(f"Clear: {result2}")

Output:

Confusing: 49
Clear: 49

Both expressions produce the same result, but the second one communicates your intent clearly without requiring the reader to recall the full precedence table. Write code for humans first, and let the computer figure out the details.

Truthiness: Python's Hidden Logic

In Python, every value has a truthiness, a concept of "true-ness" or "false-ness" that determines how it behaves in boolean contexts. When you write if x: or use x inside an and or or expression, Python doesn't check whether x literally equals True. Instead, it asks a more fundamental question: "Is this value truthy?" This is one of the most powerful, and most misunderstood, features of the language.

The rules are actually quite simple once you see the pattern. Python considers a value falsy if it represents "nothing," "empty," or "zero." Every other value is truthy. So None is falsy because it represents the absence of a value. 0, 0.0, and 0j are falsy because zero is the numeric nothing. Empty strings, lists, tuples, dicts, and sets are falsy because they contain nothing. And False is falsy because, well, it's False. Everything else, positive numbers, negative numbers, non-empty collections, non-empty strings, custom objects, is truthy by default.

The Truthiness Table

ValueTruthinessReason
NoneFalsyNothing is falsy
FalseFalsyObviously
0, 0.0, 0jFalsyZero in any numeric form
"", b""FalsyEmpty string (bytes or text)
[], (), {}, set()FalsyEmpty sequences and collections
Everything elseTruthyNon-zero, non-empty, non-None
python
# Truthiness examples
if 0:
    print("This won't print")
else:
    print("0 is falsy")
 
if []:
    print("This won't print")
else:
    print("Empty list is falsy")
 
if "":
    print("This won't print")
else:
    print("Empty string is falsy")
 
if None:
    print("This won't print")
else:
    print("None is falsy")
 
# Truthy values
if 1:
    print("1 is truthy")
 
if "hello":
    print("Non-empty string is truthy")
 
if [1, 2, 3]:
    print("Non-empty list is truthy")

Output:

0 is falsy
Empty list is falsy
Empty string is falsy
None is falsy
1 is truthy
Non-empty string is truthy
Non-empty list is truthy

Once you internalize these rules, you'll start seeing how they enable Python's most expressive idioms. The if not items: check, the value or "default" pattern, the bool(x) conversion, they all flow naturally from the truthiness rules.

Leveraging Truthiness in Your Code

Truthiness is why you can write elegant, Pythonic code. Rather than verbose comparisons like len(items) == 0 or user_name != None, you express the same intent more naturally. This isn't just style, it's a fundamental part of how Python is designed to be written:

python
# Check if list is empty
items = []
if not items:  # instead of if len(items) == 0:
    print("No items to process")
 
# Provide a default if a value is None or empty
user_input = ""
message = user_input or "No input provided"
print(message)
 
# Filter out falsy values
data = [1, 0, "hello", "", None, [1, 2], [], False, True]
cleaned = [x for x in data if x]  # Keep only truthy values
print(f"Cleaned: {cleaned}")
 
# Check if a string is not empty
name = "Alice"
if name:
    print(f"Hello, {name}!")

Output:

No items to process
No input provided
Cleaned: [1, 'hello', [1, 2], True]
Hello, Alice!

The list comprehension [x for x in data if x] is a powerful one-liner for stripping falsy values from a mixed collection. It reads almost like English: "give me each x from data, but only if x is truthy."

The "Truthy" Pitfall: Unexpected Behavior

Be careful with truthiness when you're checking for specific values. The flexibility that makes truthiness useful also introduces a class of bugs where valid values get silently treated as "no value":

python
# Gotcha: all of these are falsy in a boolean context
def process(value):
    if value:  # What if value is 0 or False intentionally?
        print(f"Processing: {value}")
    else:
        print("Value is falsy")
 
process(0)          # Says "falsy" but 0 might be a valid value!
process(False)      # Same issue
process([])         # Same issue
 
# Better: be explicit
def process_better(value):
    if value is not None:  # Only skip if None
        print(f"Processing: {value}")
    else:
        print("No value provided")
 
process_better(0)   # "Processing: 0"
process_better(False) # "Processing: False"

Output:

Value is falsy
Value is falsy
Value is falsy
Processing: 0
Processing: False

When you care about distinguishing between "no value" and "zero value," use explicit checks: if value is not None: instead of relying on truthiness. This is the critical judgment call every Python developer must make. Use implicit truthiness when you genuinely want to treat zero, empty, and None as equivalent "missing" states. Use explicit is not None checks when zero or empty string are meaningful, legitimate values in your domain.

Short-Circuit Evaluation: Writing Efficient Code

Short-circuit evaluation is when Python stops evaluating an expression as soon as it knows the result. For and, if the left side is falsy, the right side never runs. For or, if the left side is truthy, the right side never runs. We've touched on this in the logical operators section, but it deserves its own focused treatment because it's a pattern you'll rely on constantly in production code.

The practical value of short-circuit evaluation goes beyond performance. It's also a safety mechanism. When you write user and user.get_profile(), you're saying "only call get_profile() if user exists." Without short-circuiting, that expression would crash with an AttributeError when user is None. With short-circuiting, Python stops at user being falsy and never attempts the method call. This lets you write defensive guards that are both concise and correct.

python
# Practical short-circuit: avoid errors
def safe_divide(numerator, denominator):
    # If denominator is 0 (falsy), return 0 without trying division
    return (denominator != 0) and (numerator / denominator)
 
print(f"Safe divide 10 by 2: {safe_divide(10, 2)}")
print(f"Safe divide 10 by 0: {safe_divide(10, 0)}")  # Returns 0, no ZeroDivisionError!
 
# Practical short-circuit: avoid expensive operations
def is_valid_user(user_id, check_database=False):
    # Only call expensive check_database() if user_id is truthy
    return user_id and (check_database if callable(check_database) else True)
 
# check_database never runs because user_id is falsy
result = is_valid_user(None, lambda: print("Expensive check") or True)
print(f"Valid user: {result}")
 
# check_database runs because user_id is truthy
result = is_valid_user(123, lambda: print("Expensive check") or True)
print(f"Valid user: {result}")

Output:

Safe divide 10 by 2: 5.0
Safe divide 10 by 0: 0
Valid user: False
Expensive check
Valid user: True

This is how you write efficient code: structure your conditions so cheap checks happen first, and expensive operations only run if necessary. A database query is far more expensive than checking whether a string is non-empty. A network call is far more expensive than checking whether a list has items. Always put your fast, cheap guards on the left side of and expressions, and let them protect the expensive operations on the right.

The same principle applies in reverse for or: put your most-likely-to-succeed options first. If cache_lookup() succeeds 90% of the time, write cache_lookup() or database_lookup() and you'll avoid the database call in the common case.

Common Expression Mistakes

Every Python developer makes certain operator mistakes early on, and the sooner you see them catalogued, the sooner you'll stop making them. These aren't obscure edge cases, they're the bugs that appear in real code every day, sometimes surviving in production for months before anyone notices.

The first is the == versus is confusion we touched on earlier. Writing if x == None: works but is semantically wrong, you're asking "is x equal to None?" when you mean "is x the None object?" Because None is a singleton, is is always the right choice. Linters like flake8 will actually flag == None comparisons as style violations precisely for this reason.

The second is over-relying on truthiness. Writing if count: when you mean if count > 0: is subtly wrong if count could legitimately be negative. A negative count probably indicates a bug, but if your code treats it the same as "no items" because both are truthy for any non-zero value, you've silently swallowed an error. Be precise about your intent.

The third is forgetting that not has lower precedence than comparison operators, but higher precedence than and and or. The expression not a == b parses as not (a == b), which is equivalent to a != b. But not a and b parses as (not a) and b, not not (a and b). These aren't the same thing, and confusing them produces bugs that are genuinely hard to spot.

python
# Mistake 1: == vs is with None
value = None
# Wrong (works but misleading)
if value == None:
    print("Avoid this")
 
# Right
if value is None:
    print("Use this")
 
# Mistake 2: Truthiness when you need a range check
count = -5
if count:  # -5 is truthy! This incorrectly passes.
    print(f"Count is {count}")  # Prints even for negative count
 
if count > 0:  # Explicit and correct
    print(f"Count is {count}")  # Correctly skips negative
 
# Mistake 3: not precedence confusion
a, b = True, False
print(f"not a and b: {not a and b}")        # (not True) and False = False
print(f"not (a and b): {not (a and b)}")    # not (True and False) = not False = True

Output:

Use this
Count is -5
not a and b: False
not (a and b): True

The fix for all three is the same: be explicit. Use is None instead of == None. Use > 0 instead of bare truthiness when the distinction matters. Use parentheses to make operator grouping unambiguous. Explicit code is not verbose code, it's honest code. It says exactly what you mean, and it doesn't surprise the next developer who reads it.

Identity Operators: is vs ==

Two values can be equal without being the same object. The == operator checks equality; the is operator checks identity (same object in memory). This distinction is subtle but important, two different objects can contain the same data, the way two identical books are equal in content but are physically different objects on different shelves.

python
# Equality vs identity
a = [1, 2, 3]
b = [1, 2, 3]
c = a
 
print(f"a == b: {a == b}")     # True (same contents)
print(f"a is b: {a is b}")     # False (different objects in memory)
print(f"a is c: {a is c}")     # True (same object)
 
# Common mistake: using == with None
value = None
if value == None:              # Works, but...
    print("Explicitly checking with ==")
 
if value is None:              # Better! Use is with None
    print("Better: checking with is")

Output:

True
False
True
Better: checking with is

Always use is when comparing with None, True, or False. These are singletons in Python, there's only one None object in the entire program. Using is is faster and more idiomatic. This is such a strong convention in Python that static analysis tools will warn you if they see == None in your code.

python
# Singleton identity
print(f"None is None: {None is None}")           # True
print(f"True is True: {True is True}")           # True
print(f"False is False: {False is False}")       # True
 
# Small integers are cached
x = 256
y = 256
print(f"256 is 256: {x is y}")                   # True (CPython caches -5 to 256)
 
x = 257
y = 257
print(f"257 is 257: {x is y}")                   # False (not cached, different objects)

Output:

None is None: True
True is True: True
False is False: True
256 is 256: True
257 is 257: False

This last example shows a subtle Python implementation detail: CPython caches small integers (-5 to 256) for performance. Don't rely on this for arbitrary integers, always use == for value comparison and is for singleton comparisons. The integer caching is a CPython implementation detail, not a language guarantee, so code that relies on is for integer comparison might break on other Python implementations like PyPy or Jython.

Putting It All Together: A Real Example

Let's write a function that validates user input, using operators, truthiness, and short-circuit evaluation together. This example synthesizes everything we've covered into a realistic, practical function that you could drop into a real application:

python
def validate_age_input(input_string):
    """
    Validate age input: must be 18-120.
    Returns age if valid, None otherwise.
    """
    # Try to convert to int, short-circuit if it fails
    age = None
    try:
        age = int(input_string.strip()) if input_string else None
    except ValueError:
        return None
 
    # Chain comparisons: 18 <= age <= 120
    # Short-circuit: if age is None or 0, the rest doesn't evaluate
    return age if (age and 18 <= age <= 120) else None
 
# Test cases
test_inputs = ["25", "17", "150", "", "abc", None, "0"]
 
for test in test_inputs:
    result = validate_age_input(test)
    if result:
        print(f"Input '{test}' → Valid age: {result}")
    else:
        print(f"Input '{test}' → Invalid")

Output:

Input '25' → Valid age: 25
Input '17' → Invalid
Input '150' → Invalid
Input '' → Invalid
Input 'abc' → Invalid
Input 'None' → Invalid
Input '0' → Invalid

Here we see:

  • Truthiness to check if age is falsy (None, 0)
  • Comparison chaining to elegantly express the age range
  • Short-circuit evaluation so 18 <= age <= 120 only evaluates if age is truthy
  • Try-except for error handling (we'll cover this in a later article)

Summary and Next Steps

If there's one theme running through everything in this article, it's that Python's operators are designed to let you express intent clearly and efficiently. The language consistently rewards explicit, readable code over clever, terse one-liners. Use parentheses to communicate precedence. Use is for singletons. Use explicit range checks when zero is meaningful. Let short-circuit evaluation do its job by ordering conditions thoughtfully. These aren't arbitrary style rules, they're patterns that make code easier to read, debug, and extend.

The other theme is that operators in Python are richer than they first appear. and and or don't just produce True and False, they return values that let you build elegant default-assignment patterns. Every value has a truthiness that lets you write concise membership and emptiness checks. The walrus operator opens up new ways to structure loops and comprehensions. And bitwise operators, while niche, give you direct access to the binary layer when performance demands it.

You now know:

  • Arithmetic operators: division (/), floor division (//), modulo (%), and exponentiation (**)
  • Comparison operators: chaining comparisons for readability (0 < x < 10)
  • Logical operators: and, or, not, and how they short-circuit for efficiency
  • Bitwise operators: low-level bit manipulation for specific use cases
  • The walrus operator (:=): assignment expressions in Python 3.8+
  • Operator precedence: the full hierarchy and why parentheses are your best friend
  • Identity operators: is vs ==, and when to use each
  • Truthiness: what Python considers true/false, and how to leverage it correctly
  • Short-circuit evaluation: structure conditions to avoid unnecessary computation
  • Common mistakes: the pitfalls that catch everyone and how to sidestep them

Master these concepts, and you'll write cleaner, more efficient, more Pythonic code. Every future concept in this series, conditional logic, loops, functions, comprehensions, even the AI/ML work we'll get to eventually, builds on the operator and expression foundation we've laid here.

Next up is Conditional Logic in Python, where we'll use these operators to control program flow with if, elif, else, and the modern match statement. You'll discover how to structure decisions, handle multiple cases, and write code that gracefully handles different scenarios.

See you there.

Need help implementing this?

We build automation systems like this for clients every day.

Discuss Your Project