Operators
Arithmetic, comparison, logical, identity, membership, and bitwise.
What you will be able to do
- Name the seven operator families and say what each one is for
- Use the augmented assignment operators, and know what they do to mutable objects
- Explain the difference between == and is, and why is sometimes appears to work
- Combine conditions with and, or, not - and use chained comparisons
- Explain short-circuit evaluation and the None guard it makes possible
- Recognise bitwise operators and the permission-flag pattern they appear in
The idea, in plain English
An operator tells Python to do something with one or two values. The values are the operands, the symbol is the operator, and the whole thing is an expression that evaluates to a result. You have been using them since lesson one.
Python has seven families. Arithmetic you met properly in lesson 4 and this lesson will not repeat it. What is new here is everything you need for decisions: assignment shortcuts, comparison, logic, identity, membership, and - briefly - bitwise.
The reason this lesson comes before if statements is that a condition is just an expression that evaluates to True or False. Once you can read `is_active and role in ("admin", "trainer")`, the if statement around it is the easy part.
Three things in here genuinely surprise people: `is` is not `==`, `and` does not return True or False, and Python stops evaluating an expression the moment it knows the answer. All three matter in real code.
Worked example: Deciding whether a learner can enrol, from four separate conditions.
== compares values, is compares identity
== asks "do these two things have the same value?". `is` asks "are these two names pointing at the same object?". They are different questions and they usually have different answers.
Two lists with identical contents are equal but are not identical - there are two objects. This is the reference model from lesson 2, showing up as an operator.
What makes this dangerous is that `is` sometimes appears to work. CPython reuses small integers and short strings, so `256 is 256` is True while `257 is 257` may be False, and it can differ between the REPL and a script. Code that passes your test and fails in production is worse than code that always fails.
Use `is` for exactly one thing: comparing against None, True, and False, which are singletons. Everything else uses ==.
x == ySame value. The default choice for comparing anything.x is NoneThe correct None check. None is a singleton, so identity is exactly the question.x is not NoneThe negation. Note the word order - not `not x is None`.x is TrueAlmost never what you want. Write `if x:` instead, or `x == True` if you really mean the value.a is b on listsTrue only when both names refer to one list. Two equal lists are == but not is.x is 256A trap. It may be True by accident of interning, and Python 3.8+ warns about it.Watch out: Python raises a SyntaxWarning for `is` against a literal - "is" with a literal. If you see it, you meant ==.
and and or return an operand, not a boolean
This surprises everyone once. `"Python" and "Java"` evaluates to "Java", not True. `"" or "Python"` evaluates to "Python", not True.
The rule: `a and b` returns a if a is falsy, otherwise b. `a or b` returns a if a is truthy, otherwise b. When both operands happen to be booleans you get a boolean back, which is why this goes unnoticed for so long.
It powers a common idiom: `name = supplied or "Anonymous"` gives a default when supplied is empty. It also carries a bug, because 0 and "" and [] are all falsy - `port = configured or 8080` quietly replaces a deliberately configured port of 0. When None is the only value you want to replace, write the check out in full.
"a" and "b""b" - the first operand is truthy, so the second is the answer."" and "b""" - the first is falsy, so it is returned and the second is never evaluated."" or "b""b" - the first is falsy, so the second is the answer. This is the default-value idiom."a" or "b""a" - the first is truthy, so the second is never evaluated.not "a"False - not always returns a real boolean, unlike and and or.0 or 80808080 - and that is the bug, if 0 was a deliberate setting.Short-circuiting, and the guard it enables
Python stops evaluating as soon as the answer is certain. In `a and b`, if a is falsy the result is already known, so b is never evaluated at all. In `a or b`, if a is truthy the same applies.
That is not an optimisation detail - it is something you write code to depend on. `if user is not None and user.is_active:` is safe precisely because the second half never runs when user is None. Reverse the two halves and it crashes.
The same shape guards against division by zero, empty collections, and expensive calls: `if items and items[0] == x`, or `if cache_hit or expensive_lookup()`.
Tip: Order your conditions cheapest-and-safest first. The condition that prevents the error has to come before the one that would cause it.
Bitwise, and where you actually meet it
Bitwise operators work on the individual binary digits of an integer. You will not need them often, but they show up in permission systems, protocol code, and anywhere a set of on/off flags is packed into one number.
The pattern is worth recognising even if you never write it: give each permission a power of two, combine them with |, and test one with &. READ=1, WRITE=2, DELETE=4 means a value of 3 is read plus write, and `permissions & WRITE` is non-zero exactly when write is granted.
The one result that looks wrong is ~5 giving -6. Python integers are signed and conceptually infinite, so inverting every bit follows ~x == -x - 1. It is consistent, just not intuitive.
5 & 31 - AND keeps bits set in both. 0101 & 0011 = 0001.5 | 37 - OR keeps bits set in either. This is how flags are combined.5 ^ 36 - XOR keeps bits set in exactly one.~5-6 - inverts every bit. Always equals -x - 1 for signed integers.5 << 110 - shift left one place, which doubles a positive integer.20 >> 25 - shift right two places, which is floor division by 4.Watch out: & is not and. & has higher precedence than ==, so `a == 1 & b == 2` parses as `a == (1 & b) == 2`. It also never short-circuits. Use and for logic.
Syntax and examples
count = 10
count += 5 # count = count + 5
count -= 3 # 12
count *= 2 # 24
count //= 5 # 4
count **= 2 # 16
print(count) # 16
value = 100
value /= 4
print(value) # 25.0 <- /= always gives a float
# On a list, += changes the list in place; + builds a new one
a = [1, 2]
b = a
a += [3] # same object, so b sees it
print(b) # [1, 2, 3]
a = [1, 2]
b = a
a = a + [3] # new object, so b does not
print(b) # [1, 2]print(34 == 34, 34 != 30) # True True
print("apple" < "banana") # True - character by character
print("Python" == "python") # False - case matters
# Chained, which reads the way the maths does
age = 25
print(18 <= age <= 60) # True
print(0 <= 85 <= 100) # True
# Equality across types is False, ordering is an error
print(10 == "10") # False
# print(10 < "20") # TypeErrora = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True same contents
print(a is b) # False two separate list objects
print(a is c) # True one object, two names
# The trap: interning makes 'is' look correct for small values
x = 256; y = 256
print(x is y) # True - cached by CPython
x = 257; y = 257
print(x is y) # often False - and that is the point
# The only identity checks worth writing
result = None
print(result is None) # True
print(result is not None) # Falseprint("Python" and "Java") # Java - not True
print("" or "Python") # Python - the default-value idiom
print(not "Python") # False - not does return a boolean
name = ""
print(name or "Anonymous") # Anonymous
# ...and the bug hiding in that idiom
configured_port = 0
print(configured_port or 8080) # 8080 - overrode a deliberate 0
port = configured_port if configured_port is not None else 8080
print(port) # 0 - correctuser = None
# Safe: the second half never runs when the first is False
if user is not None and user.is_active:
print("active")
else:
print("no user") # no user
items = []
if items and items[0] > 10: # items[0] would raise on an empty list
print("big")
divisor = 0
if divisor != 0 and 100 / divisor > 1: # never divides by zero
print("ok")skills = ["Python", "React", "Node.js"]
print("Python" in skills) # True
print("Java" not in skills) # True
print("Py" in "Python") # substring, for strings
# On a dict, 'in' looks at the keys
user = {"name": "Chandu", "role": "Developer"}
print("name" in user) # True
print("Chandu" in user) # False - that is a value
print("Chandu" in user.values()) # True
# The clean way to test several options
role = "trainer"
print(role in ("admin", "trainer")) # better than role == "admin" or role == "trainer"READ, WRITE, DELETE = 1, 2, 4 # powers of two
permissions = READ | WRITE # combine
print(permissions) # 3 -> 0b011
print(bool(permissions & READ)) # True - granted
print(bool(permissions & DELETE)) # False - not granted
permissions |= DELETE # grant
permissions &= ~WRITE # revoke
print(f"{permissions:03b}") # 101 -> read and deleteis_logged_in = True
account_active = True
age = 25
course_available = True
role = "trainer"
# Each line says one thing, so the failing one is obvious
is_adult = 18 <= age
can_enrol = is_logged_in and account_active and is_adult and course_available
can_edit = is_logged_in and account_active and role in ("admin", "trainer")
print(f"Can enrol: {can_enrol}") # True
print(f"Can edit: {can_edit}") # TrueTip: A chained comparison evaluates the middle expression once: in `f(x) < g(y) < h(z)`, g(y) is called a single time. Writing it out with and would call it twice.
The seven families
Arithmetic+ - * / // % ** - covered in lesson 4. Remember / always returns a float.
10 // 3 # 3
Assignment= and the augmented forms += -= *= /= //= %= **=, which update in place.
count += 1
Comparison== != > < >= <= - always return a bool, and can be chained.
18 <= age <= 60
Logicaland or not - combine conditions, short-circuit, and return an operand.
a and b
Identityis, is not - same object, not same value. For None, True, False.
x is None
Membershipin, not in - is this inside that. Keys for a dict, characters for a string.
"Py" in name
Bitwise& | ^ ~ << >> - operate on the bits of an integer. Flags and low-level work.
flags & READ
Precedence, highest first
You do not need to memorise this. You need to know that comparison binds tighter than not, which binds tighter than and, which binds tighter than or - and to add parentheses everywhere else.
( )Parentheses. The only tool you need to override everything below.
**Exponentiation. Right-associative, and tighter than unary minus.
+x -x ~xUnary plus, minus, and bitwise NOT.
* / // %Multiplication and the division family.
+ -Addition and subtraction.
<< >>Bit shifts - lower than arithmetic, higher than comparison.
& ^ |Bitwise AND, XOR, OR - in that order, and all tighter than comparison.
in is < <= > >= != ==Comparison, membership and identity, all at the same level.
notLogical NOT - lower than comparison, so `not a == b` means `not (a == b)`.
andLogical AND.
orLogical OR - the loosest of all, which is why it usually needs no parentheses.
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“print("a" and "b", "" or "b", not "a")”
“x=256;y=256;print(x is y); x=257;y=257;print(x is y)”
“a=[1];b=a;a+=[2];print(b); a=[1];b=a;a=a+[2];print(b)”
“print(True & False, 1 & 2, bool(1 and 2))”
What usually goes wrong
Assignment inside an if is a SyntaxError in Python, which is a kindness - in C it silently assigns and the condition is almost always true.
✗ if age = 18:✓ if age == 18:It asks whether two names point at one object, not whether the values match. It sometimes appears to work because CPython caches small integers and short strings, which makes it worse - the bug survives testing.
✗ if username is "Chandu":✓ if username == "Chandu":It usually works, but None is a singleton so identity is the precise question, and a class can override __eq__ to make == lie. `is None` cannot be overridden.
✗ if result == None:✓ if result is None:Every falsy value takes the fallback, not just None. A configured port of 0, a quantity of 0, or an empty string that was deliberately set all get silently replaced.
✗ port = configured or 8080✓ port = 8080 if configured is None else configuredThey are bitwise operators. They do not short-circuit, and they bind tighter than == - so `a == 1 & b == 2` is parsed as `a == (1 & b) == 2`, which is not remotely what you wrote.
✗ if is_active & is_admin:✓ if is_active and is_admin:A tuple membership test says the intent directly and does not repeat the name, so it cannot go wrong when you add a third option.
✗ if role == "admin" or role == "trainer":✓ if role in ("admin", "trainer"):and binds tighter than or, so the grouping may not be what you meant. Even where it is, the parentheses tell the next reader you knew.
✗ if age >= 18 and role == "admin" or role == "trainer":✓ if age >= 18 and role in ("admin", "trainer"):Best practices
- Use == for values and reserve is for None, True, and False.
- Write `x is None`, never `x == None`.
- Use chained comparisons for ranges: 0 <= score <= 100.
- Use `in` with a tuple rather than a chain of or-ed equality tests.
- Put the guarding condition first so short-circuiting protects the one after it.
- Give complex conditions a name - can_enrol = ... - so the if statement reads as a sentence.
- Use and and or for logic, & and | only when you genuinely mean bits.
- Add parentheses whenever and and or appear in the same expression.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Given price = 5000 and discount = 0.10, work out the discount amount and the final price.
Show solutionHide solution
price = 5000
discount = 0.10
discount_amount = price * discount
final_price = price - discount_amount
print(f"Discount: {discount_amount:,.2f}") # 500.00
print(f"Final: {final_price:,.2f}") # 4,500.00Check whether age = 25 falls between 18 and 60 inclusive, using a chained comparison.
Show solutionHide solution
age = 25
print(18 <= age <= 60) # TrueGiven role = "trainer", allow access if the role is either admin or trainer - without repeating the variable.
Show hintHide hint
Membership against a tuple.
Show solutionHide solution
role = "trainer"
print(role in ("admin", "trainer")) # TrueGiven result = None, check whether it is None in the idiomatic way.
Show solutionHide solution
result = None
if result is None:
print("No result") # No resultCreate two separate lists with identical contents. Show that they are equal but not identical, then make a third name that is identical to the first.
Show solutionHide solution
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True - same contents
print(a is b) # False - two objects
print(a is c) # True - one object, two namesGiven user = None, write a condition that checks user.is_active without raising an AttributeError.
Show hintHide hint
Short-circuiting does the work - the order of the two halves is the whole answer.
Show solutionHide solution
user = None
if user is not None and user.is_active:
print("active")
else:
print("no active user") # no active userUsing READ = 1, WRITE = 2, DELETE = 4, grant read and write, then check whether delete is granted.
Show solutionHide solution
READ, WRITE, DELETE = 1, 2, 4
permissions = READ | WRITE
print(permissions) # 3
print(bool(permissions & READ)) # True
print(bool(permissions & DELETE)) # FalseAccess control for the platform
Work out what a user is allowed to do from four separate facts about them. Then run every combination below and check each answer is what you intended.
- Decide can_view from: logged in, account active, and the course published
- Decide can_edit from: logged in, account active, and a role of admin or trainer
- Use `in` with a tuple for the role test rather than or-ed comparisons
- Name each condition so the final expression reads as a sentence
- Run the five cases below and print a table of the results
cases = [
("admin", True, True, True),
("trainer", True, True, True),
("student", True, True, True),
("admin", True, False, True), # inactive account
("trainer", False, True, True), # logged out
]
for role, logged_in, active, published in cases:
...Show one solutionHide solution
EDITOR_ROLES = ("admin", "trainer")
cases = [
("admin", True, True, True),
("trainer", True, True, True),
("student", True, True, True),
("admin", True, False, True), # inactive account
("trainer", False, True, True), # logged out
]
print(f"{'role':<9}{'in':<6}{'active':<8}{'pub':<6}{'view':<7}{'edit'}")
print("-" * 42)
for role, logged_in, active, published in cases:
# One named condition per rule - the failing one is then obvious.
signed_in_and_ok = logged_in and active
can_view = signed_in_and_ok and published
can_edit = signed_in_and_ok and role in EDITOR_ROLES
print(f"{role:<9}{logged_in!s:<6}{active!s:<8}{published!s:<6}"
f"{can_view!s:<7}{can_edit!s}")
# role in active pub view edit
# ------------------------------------------
# admin True True True True True
# trainer True True True True True
# student True True True True False
# admin True False True False False
# trainer False True True False FalseKey points
- An operator acts on operands and produces a value; a condition is just an expression that evaluates to a bool.
- Augmented assignment (+=, -=, ...) updates a name in place; on a list, += mutates while + builds a new list.
- == compares values, is compares object identity. They answer different questions.
- Use is only for None, True, and False. Small-integer caching makes it appear to work elsewhere.
- Comparisons can be chained: 18 <= age <= 60, with the middle evaluated once.
- and and or return one of their operands, not necessarily a boolean. not always returns a boolean.
- `x or default` falls back on any falsy value, not just None - watch out for 0 and "".
- Short-circuiting means the second operand may never be evaluated, which is what makes None guards safe.
- `in` tests membership; on a dict it looks at the keys.
- Bitwise operators work on bits and are the basis of permission flags. & is not and.
- Precedence: comparison, then not, then and, then or. Parenthesise anything else.
Quick check before you move on
Interview questions
Why does `256 is 256` return True but `257 is 257` may not?
CPython pre-allocates and caches integer objects from -5 to 256, so every reference to 256 is the same object. 257 is created fresh each time - though the peephole optimiser may still fold two literals in the same code block, which is why the answer differs between the REPL and a script. That inconsistency is exactly the argument for never using `is` on values.
What do `and` and `or` actually return?
One of their operands. `a and b` returns a when a is falsy, otherwise b; `a or b` returns a when a is truthy, otherwise b. The truthiness of the result matches the boolean logic, which is why it goes unnoticed, and it enables `x = value or default`. `not` is the exception - it always returns a real bool.
How does `a < b < c` differ from `(a < b) < c`?
Completely. The chained form means `a < b and b < c`, with b evaluated once. The parenthesised form compares a boolean against c - since True is 1, `1 < 2 < 3` is True but `(1 < 2) < 3` is `True < 3`, which is also True for a different and accidental reason.
Why is short-circuiting part of the language rather than an optimisation?
Because programs depend on it for correctness, not speed. `user is not None and user.is_active` is only safe if the second operand is genuinely skipped. A language that evaluated both would make that idiom crash, so the behaviour has to be guaranteed rather than best-effort.
When would you use bitwise operators in application code?
Packing a set of boolean flags into one integer - file permissions, feature toggles, protocol headers - where each flag is a power of two, | grants and & tests. Also for hashing, fast multiply or divide by powers of two, and masking in binary formats. In ordinary business logic, a set or a dataclass of booleans is clearer.
What is the difference between `x += [1]` and `x = x + [1]` for a list?
+= calls __iadd__ and extends the list in place, so every other name pointing at that list sees the change. x + [1] builds a new list and rebinds x, leaving the original untouched. For immutable types like int and str there is no difference, because there is no in-place option.
Quiz
- 1.
What is the difference between = and ==?
- 2.
What is the difference between == and is?
- 3.
Why should you write `value is None` rather than `value == None`?
- 4.
What is short-circuit evaluation?
- 5.
What does `"" or "Python"` return, and why does it matter?
- 6.
Why should you not use & in place of and?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned