Python Operators: A Complete Guide

Operators are Python's action words. You cannot write a meaningful program without them. This guide teaches every operator category by building a single, working Python program from scratch — a small store inventory tracker. Each section adds new lines to that program using the operators being covered, so you see them doing actual work rather than sitting in isolated one-liners.

By the end of this article you will have a complete, runnable script. Every code block below is a piece of that script — copy them in order into a single .py file and it will run. The output blocks show exactly what each section prints.

The Program We Are Building

The program tracks inventory for a small store. It starts with a list of products, their stock counts, and their prices. Over the course of this guide we will calculate totals, apply discounts, flag low stock, filter products, and encode status flags — one operator category at a time. Here is the initial data setup:

# inventory_tracker.py — initial data
# Each product: [name, stock_count, unit_price]

products = [
    ["Widget A",  42,  4.99],
    ["Widget B",   7, 12.50],
    ["Gadget X",   0, 89.00],
    ["Gadget Y",  18, 34.75],
    ["Doohickey",  3,  2.25],
]

TAX_RATE      = 0.08    # 8%
LOW_STOCK     = 10      # threshold for low-stock warning
DISCOUNT_RATE = 0.15    # 15% bulk discount

Nothing runs yet — we have just defined our data. Every section that follows adds code below this block.

Arithmetic Operators

Python has seven arithmetic operators. Four are familiar from math class. Three are worth spending extra time on: floor division //, modulo %, and exponentiation **.

Operator Name Notes
+AdditionAlso concatenates strings and lists
-SubtractionAlso works as unary negation: -x
*MultiplicationAlso repeats strings and lists
/DivisionAlways returns float in Python 3
//Floor divisionTruncates toward negative infinity, not toward zero
%ModuloRemainder after division — sign follows the divisor in Python
**ExponentiationRight-associative: 2**3**2 = 2**(3**2) = 512

We will use arithmetic operators to calculate the total value of inventory, apply tax, and figure out how many full crates of 12 each product fills (using floor division and modulo together).

# ── Arithmetic operators ──────────────────────────────────
CRATE_SIZE = 12

print("=== Inventory Value Report ===")
for name, stock, price in products:
    line_value   = stock * price                  # multiplication
    taxed_value  = line_value + (line_value * TAX_RATE)  # addition
    full_crates  = stock // CRATE_SIZE            # floor division
    loose_units  = stock % CRATE_SIZE             # modulo (remainder)
    storage_area = stock ** (1/3)                 # cube root via fractional exponent

    print(f"  {name:<12} stock={stock:>3}  "
          f"value=${line_value:>7.2f}  "
          f"taxed=${taxed_value:>7.2f}  "
          f"crates={full_crates} + {loose_units} loose")
=== Inventory Value Report === Widget A stock= 42 value=$ 209.58 taxed=$ 226.35 crates=3 + 6 loose Widget B stock= 7 value=$ 87.50 taxed=$ 94.50 crates=0 + 7 loose Gadget X stock= 0 value=$ 0.00 taxed=$ 0.00 crates=0 + 0 loose Gadget Y stock= 18 value=$ 625.50 taxed=$ 675.54 crates=1 + 6 loose Doohickey stock= 3 value=$ 6.75 taxed=$ 7.29 crates=0 + 3 loose
Floor division behavior with negatives

Floor division rounds toward negative infinity, not toward zero. 7 // 2 is 3, but -7 // 2 is -4 — not -3. If you need truncation toward zero, use int(-7 / 2) instead. For inventory counts this never matters, but it bites people writing date/time arithmetic.

Augmented Assignment Operators

Augmented assignment operators collapse a read-modify-write into one line. Every arithmetic and bitwise operator has an augmented form: += -= *= /= //= %= **= &= |= ^= <<= >>=.

One important subtlety: for mutable types like lists, x += [item] mutates the list in place (same as x.extend([item])). For immutable types like integers and strings, it creates a new object and rebinds the name. This distinction matters when multiple variables point at the same object.

# ── Augmented assignment operators ────────────────────────
# Simulate receiving a shipment and selling stock

total_revenue = 0.0
total_units_sold = 0

# Shipment arrives: +20 units of Widget B, +15 of Gadget X
products[1][1] += 20    # Widget B stock: 7 + 20 = 27
products[2][1] += 15    # Gadget X stock: 0 + 15 = 15

# Price increase: Gadget X goes up 10%
products[2][2] *= 1.10  # 89.00 * 1.10 = 97.90

# Process some sales
sales = [("Widget A", 5), ("Gadget X", 3), ("Doohickey", 1)]

for sale_name, qty in sales:
    for product in products:
        if product[0] == sale_name:
            revenue    = qty * product[2]
            product[1] -= qty           # reduce stock
            total_revenue += revenue    # accumulate revenue
            total_units_sold += qty     # accumulate units

print(f"\nAfter shipment and sales:")
print(f"  Total revenue:    ${total_revenue:.2f}")
print(f"  Total units sold: {total_units_sold}")

# Show updated stock
for name, stock, price in products:
    print(f"  {name:<12} stock now = {stock}")
After shipment and sales: Total revenue: $318.20 Total units sold: 9 Widget A stock now = 37 Widget B stock now = 27 Gadget X stock now = 12 Gadget Y stock now = 18 Doohickey stock now = 2
Pro Tip

+= on a string builds a new string object every time because strings are immutable. Inside a loop this generates a lot of garbage. If you are accumulating many strings, collect them in a list and join at the end: "".join(parts) is dramatically faster for large inputs.

Comparison Operators

Comparison operators evaluate a relationship between two values and return True or False. Python supports six: == != > < >= <=. They also chain — 0 <= stock < 10 is a single expression that checks both bounds at once.

We will use comparisons to classify each product's stock status and to find anything that needs reordering.

# ── Comparison operators ──────────────────────────────────
REORDER_POINT = 5

print("\n=== Stock Status ===")
for name, stock, price in products:
    # Chained comparison: checks both bounds in one expression
    if 0 < stock <= LOW_STOCK:
        status = "LOW"
    elif stock == 0:
        status = "OUT"
    elif stock > LOW_STOCK:
        status = "OK"
    else:
        status = "UNKNOWN"

    needs_reorder = stock <= REORDER_POINT   # boolean into a variable
    flag = " ** REORDER **" if needs_reorder else ""

    print(f"  {name:<12} stock={stock:>3}  status={status:<7}{flag}")

# Find the most expensive in-stock product
# Comparison used to drive a running maximum
most_expensive_name  = None
most_expensive_price = 0.0

for name, stock, price in products:
    if stock > 0 and price > most_expensive_price:
        most_expensive_price = price
        most_expensive_name  = name

print(f"\n  Most expensive in-stock: {most_expensive_name} (${most_expensive_price:.2f})")
=== Stock Status === Widget A stock= 37 status=OK Widget B stock= 27 status=OK Gadget X stock= 12 status=OK Gadget Y stock= 18 status=OK Doohickey stock= 2 status=LOW ** REORDER ** Most expensive in-stock: Gadget X ($97.90)
Comparing floats

Never use == to compare floating-point numbers for exact equality. 0.1 + 0.2 == 0.3 returns False in Python because of how binary floating-point works. Use abs(a - b) < 1e-9 or math.isclose(a, b) when you need float equality. For money calculations, use the decimal module instead of floats.

Logical Operators and Short-Circuiting

Python's logical operators — and or not — combine boolean expressions. What surprises many beginners is that they do not return True or False directly. They return one of their operands. This makes them useful for more than just if statements.

# ── Logical operators + short-circuit evaluation ──────────
print("\n=== Discount Eligibility ===")

# Bulk discount: in stock AND price over $10 AND quantity over 15
for name, stock, price in products:
    eligible = stock > 0 and price > 10.00 and stock > 15
    if eligible:
        discounted = price * (1 - DISCOUNT_RATE)
        print(f"  {name:<12}  ${price:.2f} -> ${discounted:.2f}  (bulk discount applied)")

# 'or' for safe default — if name is falsy, fall back to "Unknown"
override_name = ""
display_name = override_name or "Unknown Product"
print(f"\n  Fallback name demo: '{display_name}'")

# 'not' to invert a flag
out_of_stock_items = [name for name, stock, _ in products if not stock > 0]
print(f"  Out-of-stock items: {out_of_stock_items or 'none'}")

# Short-circuit: right side never runs if left side settles it
def expensive_check():
    print("  (expensive_check ran)")
    return True

flag = False and expensive_check()   # expensive_check() is never called
print(f"\n  Short-circuit with 'and': flag={flag}  (no call above)")

flag = True or expensive_check()     # expensive_check() is never called
print(f"  Short-circuit with 'or' : flag={flag}  (no call above)")
=== Discount Eligibility === Widget B $12.50 -> $10.62 (bulk discount applied) Gadget Y $34.75 -> $29.54 (bulk discount applied) Fallback name demo: 'Unknown Product' Out-of-stock items: none Short-circuit with 'and': flag=False (no call above) Short-circuit with 'or' : flag=True (no call above)

The short-circuit behavior matters in real code. When the left side of and is False, Python skips the right side entirely — the result cannot change. This means you can safely guard against errors like x is not None and x.value > 0; if x is None, the attribute access on the right never runs.

# Safe attribute access via short-circuit guard
class Product:
    def __init__(self, name, stock):
        self.name  = name
        self.stock = stock

p = None
# Without guard this would raise AttributeError:
# print(p.stock > 0)

# With short-circuit guard — safe:
has_stock = p is not None and p.stock > 0
print(f"\n  Safe guard result: {has_stock}")

p = Product("Gadget Z", 5)
has_stock = p is not None and p.stock > 0
print(f"  With real object:  {has_stock}")
Safe guard result: False With real object: True

Identity and Membership Operators

is and is not check object identity — whether two names point to the same object in memory, not whether their values are equal. in and not in check whether a value exists inside a container.

# ── Identity operators (is / is not) ──────────────────────
print("\n=== Identity vs Equality ===")

a = [1, 2, 3]
b = [1, 2, 3]
c = a           # c points at the same list object as a

print(f"  a == b : {a == b}   (same values)")
print(f"  a is b : {a is b}  (different objects in memory)")
print(f"  a is c : {a is c}   (same object — c is an alias)")

# Mutating via alias — this is why 'is' matters
c.append(4)
print(f"  After c.append(4): a = {a}")  # a changed too!

# Correct use of 'is': checking for None
def find_product(target_name):
    for product in products:
        if product[0] == target_name:
            return product
    return None   # explicit sentinel value

result = find_product("Widget A")
if result is not None:            # correct None check
    print(f"\n  Found: {result[0]}, stock={result[1]}")

result = find_product("Widget Z")
if result is None:
    print(f"  Widget Z not found (result is None: {result is None})")
=== Identity vs Equality === a == b : True (same values) a is b : False (different objects in memory) a is c : True (same object — c is an alias) After c.append(4): a = [1, 2, 3, 4] Found: Widget A, stock=37 Widget Z not found (result is None: True)
# ── Membership operators (in / not in) ────────────────────
print("\n=== Membership Checks ===")

product_names = [p[0] for p in products]
premium_names = {"Gadget X", "Gadget Y"}   # set — O(1) lookup

search_targets = ["Widget A", "Widget Z", "Gadget X"]

for target in search_targets:
    in_inventory = target in product_names      # list — O(n) scan
    is_premium   = target in premium_names      # set  — O(1) lookup
    print(f"  {target:<12}  in_inventory={in_inventory}  is_premium={is_premium}")

# 'in' on a string checks for substrings
keyword = "Gadget"
gadgets = [name for name in product_names if keyword in name]
print(f"\n  Products matching '{keyword}': {gadgets}")

# 'not in' in a guard
restricted = {"Gadget X"}
print("\n  Sale eligibility:")
for name, stock, price in products:
    if name not in restricted and stock > 0:
        print(f"    {name} is available for sale")
=== Membership Checks === Widget A in_inventory=True is_premium=False Widget Z in_inventory=False is_premium=False Gadget X in_inventory=True is_premium=True Products matching 'Gadget': ['Gadget X', 'Gadget Y'] Sale eligibility: Widget A is available for sale Widget B is available for sale Gadget Y is available for sale Doohickey is available for sale

Bitwise Operators

Bitwise operators manipulate numbers at the binary level. They are used in systems programming, networking, cryptography, and anywhere compact flag storage is needed. The six operators are: & (AND) | (OR) ^ (XOR) ~ (NOT) << (left shift) >> (right shift).

We will use a bitfield to encode multiple boolean properties of each product into a single integer — a technique common in low-level code, protocol parsing, and database flag columns.

# ── Bitwise operators — product status flags ──────────────
# We encode four boolean properties into a single integer.
# Each property occupies one bit position (a power of 2).

FLAG_IN_STOCK    = 1 << 0   # bit 0 = 0b0001 = 1
FLAG_LOW_STOCK   = 1 << 1   # bit 1 = 0b0010 = 2
FLAG_PREMIUM     = 1 << 2   # bit 2 = 0b0100 = 4
FLAG_ON_SALE     = 1 << 3   # bit 3 = 0b1000 = 8

print(f"\n  Flag values (decimal): IN_STOCK={FLAG_IN_STOCK} "
      f"LOW={FLAG_LOW_STOCK} PREMIUM={FLAG_PREMIUM} SALE={FLAG_ON_SALE}")

sale_items   = {"Widget B", "Gadget Y"}
premium_set  = {"Gadget X", "Gadget Y"}

print("\n=== Bitfield Status Encoding ===")
status_map = {}

for name, stock, price in products:
    flags = 0  # start with no flags set

    if stock > 0:
        flags |= FLAG_IN_STOCK      # set bit 0 with OR

    if 0 < stock <= LOW_STOCK:
        flags |= FLAG_LOW_STOCK     # set bit 1

    if name in premium_set:
        flags |= FLAG_PREMIUM       # set bit 2

    if name in sale_items:
        flags |= FLAG_ON_SALE       # set bit 3

    status_map[name] = flags
    print(f"  {name:<12}  flags={flags:>2}  binary={flags:04b}")

# Now read the flags back using AND to test individual bits
print("\n=== Decoding Flags ===")
for name, flags in status_map.items():
    in_stock  = bool(flags & FLAG_IN_STOCK)    # is bit 0 set?
    low       = bool(flags & FLAG_LOW_STOCK)   # is bit 1 set?
    premium   = bool(flags & FLAG_PREMIUM)     # is bit 2 set?
    on_sale   = bool(flags & FLAG_ON_SALE)     # is bit 3 set?

    attrs = []
    if in_stock : attrs.append("in-stock")
    if low      : attrs.append("LOW")
    if premium  : attrs.append("premium")
    if on_sale  : attrs.append("on-sale")
    print(f"  {name:<12}  {', '.join(attrs) or 'out of stock'}")

# XOR to toggle the ON_SALE flag for Widget A
name = "Widget A"
status_map[name] ^= FLAG_ON_SALE
print(f"\n  After toggling ON_SALE for Widget A: flags={status_map[name]:04b}")
status_map[name] ^= FLAG_ON_SALE   # toggle back
print(f"  After toggling again:                flags={status_map[name]:04b}")

# Right shift to divide by powers of 2 efficiently
bulk_price = 97.90
half_price = int(bulk_price * 100) >> 1   # divide cents integer by 2
print(f"\n  Integer right-shift halving: {int(bulk_price * 100)} >> 1 = {half_price} cents")
Flag values (decimal): IN_STOCK=1 LOW=2 PREMIUM=4 SALE=8 === Bitfield Status Encoding === Widget A flags= 1 binary=0001 Widget B flags= 9 binary=1001 Gadget X flags= 5 binary=0101 Gadget Y flags=13 binary=1101 Doohickey flags= 3 binary=0011 === Decoding Flags === Widget A in-stock Widget B in-stock, on-sale Gadget X in-stock, premium Gadget Y in-stock, premium, on-sale Doohickey in-stock, LOW After toggling ON_SALE for Widget A: flags=1001 After toggling again: flags=0001 Integer right-shift halving: 9790 >> 1 = 4895 cents
Python 3.9+: | merges dictionaries too

Since Python 3.9, the | operator also merges two dictionaries: merged = dict_a | dict_b. This is separate from the bitwise OR behavior — Python checks the operand types and routes to the right operation. The augmented form dict_a |= dict_b updates in place.

The Walrus Operator

The walrus operator := was introduced in Python 3.8. It assigns a value and returns it in the same expression. The name comes from the way it looks — the colon and equals together resemble a walrus face sideways. Its formal name is the assignment expression operator.

It is most useful in two places: while loops where you need to capture a computed value and test it in one step, and list comprehensions where computing a value for both the condition and the output would otherwise require computing it twice.

# ── Walrus operator (:=) ──────────────────────────────────
import math

print("\n=== Walrus Operator Examples ===")

# Example 1: while loop — read commands until quit
commands = ["check Widget A", "check Gadget Y", "quit", "check Widget B"]
idx = 0

while (cmd := commands[idx]) != "quit":
    # cmd is already assigned — no need to re-read
    parts = cmd.split()
    search = " ".join(parts[1:])
    for name, stock, price in products:
        if name == search:
            print(f"  [{cmd}] -> stock={stock}, price=${price:.2f}")
    idx += 1

print(f"  Stopped at command: '{cmd}'")

# Example 2: comprehension — compute a value once, use it twice
# Without walrus you would compute len(name) twice:
#   [name for name in product_names if len(name) > 8]
# With walrus:
long_names = [
    (name, n)
    for name in product_names
    if (n := len(name)) > 8
]
print(f"\n  Products with name longer than 8 chars:")
for name, length in long_names:
    print(f"    '{name}' ({length} chars)")

# Example 3: find the first product under $5 using a while loop
idx = 0
while idx < len(products) and (p := products[idx])[2] >= 5.00:
    idx += 1

if idx < len(products):
    print(f"\n  First product under $5: {p[0]} (${p[2]:.2f})")
=== Walrus Operator Examples === [check Widget A] -> stock=37, price=$4.99 [check Gadget Y] -> stock=18, price=$34.75 Stopped at command: 'quit' Products with name longer than 8 chars: 'Doohickey' (9 chars) First product under $5: Widget A ($4.99)
When not to use :=

The walrus operator is useful in loops and comprehensions, but overusing it inside regular expressions makes code harder to read. If a plain assignment on its own line is clearer, use that. The walrus operator is a tool for specific situations — not a shorthand to apply everywhere.

Operator Precedence and the Full Program

When an expression contains multiple operators, Python follows a strict evaluation order. Expressions in parentheses are always evaluated first. After that, the order from highest to lowest precedence is:

Priority Operators
1 — highest() — parentheses
2** — exponentiation (right-associative)
3+x -x ~x — unary operators
4* / // %
5+ -
6<< >> — bitwise shifts
7& — bitwise AND
8^ — bitwise XOR
9| — bitwise OR
10== != > < >= <= is is not in not in
11not — logical NOT
12and — logical AND
13 — lowestor — logical OR

A practical example where precedence produces surprising results — and where parentheses fix it:

# ── Precedence in practice ────────────────────────────────
stock = 7
price = 12.50

# Intended: (stock > 0) and (price > 10)
# Without parens — works as intended because comparison > and
result_a = stock > 0 and price > 10
print(f"\n  stock > 0 and price > 10  -> {result_a}")

# Tricky: mixing bitwise and comparison — always use parens here
flags  = 0b0101   # IN_STOCK + PREMIUM
is_ok  = flags & FLAG_IN_STOCK != 0   # WRONG: parsed as flags & (FLAG_IN_STOCK != 0)
is_ok2 = (flags & FLAG_IN_STOCK) != 0  # CORRECT
print(f"  Without parens (wrong): {is_ok}")
print(f"  With parens (correct):  {is_ok2}")

# Exponentiation is right-associative — matters more than you'd think
print(f"\n  2 ** 3 ** 2  = {2 ** 3 ** 2}")    # 2**(3**2) = 2**9 = 512
print(f"  (2 ** 3) ** 2 = {(2 ** 3) ** 2}")  # 8**2 = 64

# Final summary report using multiple operator types together
print("\n=== Final Inventory Summary ===")
total_value  = sum(s * p for _, s, p in products)
total_taxed  = total_value * (1 + TAX_RATE)
in_stock_qty = sum(s for _, s, _ in products if s > 0)
product_count = len(products)
avg_price    = sum(p for _, _, p in products) / product_count

print(f"  Products tracked:  {product_count}")
print(f"  Total units:       {in_stock_qty}")
print(f"  Inventory value:   ${total_value:.2f}")
print(f"  With tax ({TAX_RATE*100:.0f}%):   ${total_taxed:.2f}")
print(f"  Avg unit price:    ${avg_price:.2f}")
print(f"  Low stock items:   {sum(1 for _, s, _ in products if 0 < s <= LOW_STOCK)}")
stock > 0 and price > 10 -> True Without parens (wrong): True With parens (correct): True 2 ** 3 ** 2 = 512 (2 ** 3) ** 2 = 64 === Final Inventory Summary === Products tracked: 5 Total units: 96 Inventory value: $1261.13 With tax (8%): $1362.02 Avg unit price: $28.09 Low stock items: 1
"When code and comments disagree, both are probably wrong." — Norm Schryer

The same principle applies to operator precedence: when you are not certain which sub-expression evaluates first, add parentheses. They make your intent explicit and protect the code against misreading — by you, by colleagues, and by future versions of yourself six months from now.

Key Takeaways

  1. Division always returns a float: Use // when you need an integer result. Pair it with % to extract both the quotient and the remainder together.
  2. Augmented assignment (+= etc.) is not purely syntactic sugar: For mutable types like lists it mutates in place; for immutable types it rebinds the name. Know which you are working with.
  3. Logical operators return values, not booleans: or is the standard Python idiom for defaults (x or "fallback"). Short-circuit evaluation means the right side can be a guard, not just a value.
  4. Use is only for None, True, and False: For everything else use ==. The small-integer cache makes is appear to work on numbers in the interpreter, but it is not guaranteed.
  5. Bitwise operators are about binary representations: Bitfields let you store many boolean flags in a single integer and test or toggle them individually using &, |, and ^.
  6. Add parentheses whenever you mix bitwise and comparison operators: (flags & MASK) != 0 is safer and clearer than relying on precedence rules that most readers will not recall under pressure.
  7. The walrus operator (:=) has two natural homes: while loops where you need to capture and test in one step, and comprehensions where an intermediate value would otherwise be computed twice.

You now have a complete, runnable program that exercises every operator category Python provides. Paste the code blocks in order into a single file and run it — the output should match every section above. From here, the next natural step is Python's control flow structures, where these operators do their most important work.