a = 100
b = 100
print(f"a == b: {a == b}") # Output: a == b: True
print(f"a is b: {a is b}") # Output: a is b: True (for integers -5 to 256)
c = 300
d = 300
print(f"c == d: {c == d}") # Output: c == d: True
print(f"c is d: {c is d}") # Output: c is d: False (for integers outside -5 to 256)
s1 = "hello"
s2 = "hello"
print(f"s1 is s2: {s1 is s2}") # Output: s1 is s2: True (string interning for short, simple strings)
s3 = "hello world!"
s4 = "hello world!"
print(f"s3 is s4: {s3 is s4}") # Output: s3 is s4: False (interring not guaranteed for complex strings)
CPython pre-allocates and caches integer objects in the range of -5 to 256. Similarly, short, simple string literals are often "interned" for performance. This means that multiple references to these specific values will point to the same object in memory, making
is return True. This is an implementation detail and should not be relied upon for general equality checks, where == is the correct semantic choice.Mutable Default Arguments
A common pitfall for new and experienced developers alike arises from mutable objects used as default arguments in function definitions. Default arguments are evaluated once when the function is defined, not on each call.
def add_item_to_list(item, data=[]):
data.append(item)
return data
list1 = add_item_to_list(1)
print(f"List 1: {list1}") # Output: List 1: [1]
list2 = add_item_to_list(2)
print(f"List 2: {list2}") # Output: List 2: [1, 2] - Unobvious! `data` is the same list object as before.
list3 = add_item_to_list(3, []) # Passed a new list
print(f"List 3: {list3}") # Output: List 3: [3]
print(f"List 2 after List 3: {list2}") # Output: List 2 after List 3: [1, 2] - Unchanged.
The "unobvious" part is that
data in the list2 call is the same list object that was modified by list1. The standard workaround is to use None as a sentinel value:def add_item_to_list_safe(item, data=None):
if data is None:
data = []
data.append(item)
return data
list4 = add_item_to_list_safe(1)
print(f"List 4 (safe): {list4}") # Output: List 4 (safe): [1]
list5 = add_item_to_list_safe(2)
print(f"List 5 (safe): {list5}") # Output: List 5 (safe): [2] - Now as expected.
Chained Comparisons
Python allows for elegant chained comparisons, which can sometimes surprise those accustomed to other languages that require explicit logical operators (
and, &&).x = 7
# Traditional (and verbose)
if 0 < x and x < 10:
print("x is between 0 and 10 (exclusive) - traditional")
# Python's elegant chained comparison
if 0 < x < 10:
print("x is between 0 and 10 (exclusive) - chained")
# More complex chaining
a, b, c = 1, 2, 3
if a < b == c:
print("a is less than b, and b is equal to c") # False, since b != c
This unobvious syntactic sugar evaluates from left to right, short-circuiting if any comparison is false. It is equivalent to
(0 < x) and (x < 10) but offers a cleaner, more mathematical notation.The Walrus Operator (
:=)Introduced in Python 3.8, the assignment expression operator
:=, informally known as the "walrus operator," allows you to assign a value to a variable as part of an expression. This can lead to more concise code in situations where you would otherwise repeat an expression or assign it on a separate line.