Skip to content
1stepGrowLearnCompareGrow

Python Operators: What Each One Returns, and Where It Bites

Python operators explained with outputs for every example, plus why is works for 256 but fails for 257 and why and/or rarely return booleans.

Sivaranjani S

Cloud Operations Engineer, Zoho

9 min readUpdated
Share
On this page

Python operators are the symbols and keywords that combine values: arithmetic, comparison, logical, membership, identity, bitwise and assignment. Most take a minute to learn. Three cause real bugs, though: is passing for 256 and failing for 257, and/or returning an operand instead of True/False, and += quietly changing a list that another name also points to.

This guide is for anyone who writes basic Python and wants to predict what an expression returns before running it. Every example shows its output, and the last section lists the five operator mistakes worth checking for in code review. It sits between Python strings and user-defined functions in the Python series.

Python operators at a glance

Category Operators Returns
Arithmetic + - * / // % ** @ a number (@ is matrix multiplication, used by NumPy)
Comparison == != < <= > >= bool
Logical and or not an operand (not returns bool)
Identity is, is not bool
Membership in, not in bool
Bitwise & | ^ ~ << >> an int (elementwise in NumPy and pandas)
Assignment = += -= *= := nothing, except := which returns the value

Arithmetic operators: why / returns a float

The arithmetic operators behave like a calculator except for division: / always returns a float, and // rounds down toward negative infinity rather than toward zero.

Python
7 + 3       # 10
7 - 3       # 4
7 * 3       # 21
7 / 3       # 2.3333333333333335   - always a float
7 // 3      # 2                    - floor division
7 % 3       # 1                    - remainder
7 ** 3      # 343                  - power

Two things surprise people.

/ always returns a float, even when it divides evenly. For example, 6 / 3 is 2.0, not 2. Use // when you want an integer.

// floors toward negative infinity, which is not truncation:

Python
7 // 2      # 3
-7 // 2     # -4     - not -3
int(-7 / 2) # -3     - truncation, if that is what you wanted

The modulo follows the same rule, so -7 % 2 is 1, not -1. This is deliberate and useful, because x % n always lands in [0, n) for positive n, which makes it safe for cyclic indexing.

Also watch floating point:

Python
0.1 + 0.2            # 0.30000000000000004
0.1 + 0.2 == 0.3     # False

import math
math.isclose(0.1 + 0.2, 0.3)     # True

For money, use decimal.Decimal — see our Python numeric data types guide.

How do chained comparisons work?

A chain such as a < b <= c means a < b and b <= c, except that b is evaluated only once:

Python
5 == 5.0      # True  - value equality across types
5 != 3        # True
3 < 5 <= 5    # True  - chained, reads mathematically

The comparisons section of the language reference defines this exactly. Chaining also short-circuits, so 0 <= index < len(items) is both idiomatic and efficient. Writing 0 <= index and index < len(items) is longer and no clearer.

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

== compares value, while is compares identity. This distinction produces real bugs.

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

a == b      # True  - same contents
a is b      # False - different objects
a is c      # True  - same object

Now the trap:

Python
x = 256
y = int("256")
x is y      # True  - small ints are cached

x = 257
y = int("257")
x is y      # False - a fresh object

As of September 2026 (Python 3.14.7), the CPython integer-object documentation still describes an array of pre-allocated integers from -5 to 256, so small values share objects, while larger ones usually do not. The same applies to short strings via interning. To make things murkier, if you write x = 257 and y = 257 in the same script, the compiler may reuse one constant and x is y becomes True. Since Python 3.8, writing x is 257 directly also triggers a SyntaxWarning asking whether you meant ==.

As a result, is on numbers appears to work in testing and fails in production with real data. Use == for values.

Reserve is for None and other singletons, as PEP 8's programming recommendations advise:

Python
value = None
if value is None:
    print("missing")
if value is not None:
    print("present")

flag = True
if flag:                  # not "if flag is True" or "if flag == True"
    print("enabled")

if value == None works but is non-idiomatic, and linters will flag it. Similarly, PEP 8 calls if flag is True: worse than == True; a plain truth test is the right form.

What do and and or return in Python?

and and or do not return True/False. Instead, they return one of their operands, as the boolean operations reference states:

Python
"hello" and "world"     # 'world'  - both truthy, returns the last
"" and "world"          # ''       - first is falsy, short-circuits
"hello" or "world"      # 'hello'  - first truthy wins
"" or "default"         # 'default'
None or 0 or "found"    # 'found'  - first truthy

That behaviour gives the default-value idiom:

Python
user_input = ""
name = user_input or "Anonymous"          # 'Anonymous'

There is one important caveat: this treats every falsy value as absent. If 0 or "" are legitimate values, use an explicit None check:

Python
config = {"timeout": 0}

# bug: a timeout of 0 becomes 30
timeout = config.get("timeout") or 30     # 30

# correct
timeout = config.get("timeout")
if timeout is None:
    timeout = 30                          # stays 0

Short-circuiting also guards against errors:

Python
user = None
data = []
if user is not None and user.is_active:      # safe - no AttributeError
    print("active")
if data and data[0] > 10:                     # no IndexError on empty
    print("big first value")

How do in and not in work?

in tests containment: an element of a list or set, a key of a dict, or a substring of a string. not in is its negation.

Python
3 in [1, 2, 3]              # True
"key" in {"key": 1}         # True - checks KEYS, not values
"ell" in "hello"            # True - substring test
5 not in {1, 2, 3}          # True

Note the complexity difference, though. in on a list is O(n), whereas on a set or dict it is O(1) on average. Converting a list to a set before repeated membership tests is therefore one of the easiest performance wins there is:

Python
id_list = [101, 102, 103]
valid_ids = set(id_list)                           # once
[r for r in [101, 999, 103] if r in valid_ids]     # [101, 103]

Bitwise operators, and the pandas gotcha

Python
5 & 3       # 1   - AND
5 | 3       # 7   - OR
5 ^ 3       # 6   - XOR
~5          # -6  - NOT
5 << 1      # 10  - left shift

These matter in pandas and NumPy, where &, | and ~ are the elementwise operators and and/or/not fail:

Python
df[(df.age > 25) & (df.city == "Pune")]     # correct
df[df.age > 25 and df.city == "Pune"]        # ValueError: truth value is ambiguous

The parentheses are required because & binds more tightly than >. See our NumPy indexing guide for the full explanation.

Augmented assignment: += is not always a rebind

For immutable types, x += 1 builds a new object and rebinds the name. For a list, however, += extends the existing object in place:

Python
a = [1, 2]
b = a
a += [3]
print(b)      # [1, 2, 3] - b sees the change

a = [1, 2]
b = a
a = a + [3]
print(b)      # [1, 2]    - a now names a new list

This is the same names-versus-objects model covered in Python basics.

When is the walrus operator (:=) worth using?

Use := when you need to test a value and keep it in the same step. It assigns inside an expression, and was added in Python 3.8. The language reference calls it an assignment expression:

Python
import re
pattern = re.compile(r"id=(\d+)")
line = "user id=42"

# without
match = pattern.search(line)
if match:
    print(match.group(1))

# with
if (match := pattern.search(line)):
    print(match.group(1))     # 42

It is genuinely useful in while loops and comprehensions where you would otherwise compute twice:

Python
import io
f = io.StringIO("x" * 20000)
while (chunk := f.read(8192)):
    print(len(chunk))         # 8192, 8192, 3616

data = ["1", "x", "3"]
results = [n for s in data if (n := int(s) if s.isdigit() else None) is not None]
# [1, 3]

Use it where it removes duplication. Do not use it to compress two clear lines into one dense one.

Which operator runs first? Precedence in practice

Among the common Python operators, ** binds tightest, then unary minus, then *, /, // and %, then + and -; comparisons come next, and not, and and or bind loosest. Most surprises come from the edges of that order:

Python
2 + 3 * 4        # 14, not 20
2 ** 3 ** 2      # 512  - power is right-associative: 2**(3**2)
-2 ** 2          # -4   - unary minus binds looser than ** on its left
not True == False   # True - == binds tighter than not

The full operator precedence table is in the language reference. Still, the practical advice is not to memorise it. Add parentheses when the answer is not instantly obvious to a reader, because it costs nothing and prevents an entire class of bug.

Common mistakes with Python operators

  • Using is to compare numbers or strings. It depends on caching; use ==.
  • Using and/or on pandas Series. Use &/| with parentheses.
  • Relying on or for defaults when 0 is valid. Check is None instead.
  • Expecting / to return an int. Use //, and remember it floors negatives.
  • Forgetting that += mutates lists in place. Other names bound to the list see the change.

Next in the series, user-defined functions shows how to package these expressions into reusable code.

The Sunday Growth Brief

One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.

No spam. Unsubscribe in one click.

Python numeric types covers int, float and Decimal in depth. Python basics covers the core types these operate on.

Frequently asked questions

What is the difference between == and is?

== asks whether two objects have the same value; is asks whether they are literally the same object in memory. Use == for comparisons and reserve is for None and other singletons such as sentinel objects. For booleans, PEP 8 recommends plain truth tests like if flag: rather than comparing with is True or == True.

Why does `a is b` return True for 256 but False for 257?

CPython pre-allocates small integers from -5 to 256, so identical small values share one object. Larger integers usually get fresh objects, although the compiler may reuse a constant within one script. This is an implementation detail, not a language guarantee, and it is exactly why you should never use is to compare numbers.

What does the walrus operator do?

The walrus operator, :=, assigns a value inside an expression and was added in Python 3.8. It helps when you need to both test and keep a value, such as if (m := pattern.search(line)):, which avoids a separate assignment line and a second search. Use it where it removes duplication, not to squeeze two clear lines into one.

What is the difference between / and //?

/ is true division and always returns a float, even when the result is whole. // is floor division, returning the largest integer less than or equal to the result. Note that it floors toward negative infinity, so -7 // 2 is -4, not -3. If you want truncation toward zero instead, use int(-7 / 2).

Written by

Sivaranjani S

Cloud Operations Engineer, Zoho

Cloud operations engineer and technical writer at Zoho, previously technical content consultant at 1stepGrow Academy.

Cloud operationsLinuxSaaSTechnical writing

Related reading

Free weekly newsletter

Get the shortlist before everyone else

Every Sunday we send one email with the week’s sharpest course comparison, a career roadmap worth stealing, and the tech news and hiring signals we’re watching.

  • No fluff, ever
  • Unsubscribe anytime
  • 5-minute read

We never share your address. One click to leave.