I have a difficult relationship with Python, but I've been writing Python for years. I mean, we all know list, dict, str. But do we actually know it works?
Here are 5 facts about Python's built-in types that will make you question everything.
Quick Note:
After 9 years of battles between me and Python i finally decided to fully comeback to him.
I will publish some facts, tutorials and other fun stuff here.
Well, let's jump to the first fact.
1. True + True == 2 (and it's not a bug)
>>> True + True
2
>>> True * 5
5
>>> isinstance(True, int)
True
Wait, what?
bool is a subclass of int. True is literally 1, and False is literally 0.
>>> issubclass(bool, int)
True
>>> True == 1
True
>>> False == 0
True
This isn't a quirk - it's intentional. In Python, booleans were added after integers, and making them a subclass preserved backward compatibility.
Practical implications:
# This works
numbers = [1, 2, 3, True, False]
sum(numbers) # 6
# So does this
conditions = [True, False, True, True]
sum(conditions) # Count of True values: 3
You can literally use sum() to count boolean conditions. Mind blown? π€―
2. String Interning: Why "wtf" is "wtf" but "wtf!" is not "wtf!"
>>> a = "hello"
>>> b = "hello"
>>> a is b
True # Same object!
>>> a = "hello!"
>>> b = "hello!"
>>> a is b
False # Different objects!
Python "interns" certain strings - it reuses the same object in memory for identical strings. But only for strings that look like identifiers (letters, numbers, underscores).
>>> a = "python"
>>> b = "python"
>>> id(a) == id(b)
True # Same memory address
>>> a = "hello world"
>>> b = "hello world"
>>> id(a) == id(b)
False # Different memory addresses
Why does this matter?
It doesn't, usually. But if you're using is instead of == for string comparison, you're gonna have a bad time.
# WRONG
if user_input is "admin": # Don't do this
grant_access()
# RIGHT
if user_input == "admin":
grant_access()
Pro tip: Use is only for None, True, and False. For everything else, use ==.
3. Dicts Remember Order (Since Python 3.7, But Nobody Noticed)
>>> d = {"z": 1, "a": 2, "m": 3}
>>> list(d.keys())
['z', 'a', 'm'] # Insertion order preserved!
Before Python 3.7, dict order was random. You'd get different results every time.
# Python 3.6 and earlier
>>> d = {"z": 1, "a": 2, "m": 3}
>>> list(d.keys())
['a', 'z', 'm'] # Random!
# Python 3.7+
>>> d = {"z": 1, "a": 2, "m": 3}
>>> list(d.keys())
['z', 'a', 'm'] # Guaranteed insertion order
This changed everything:
# Now you can use dict as an ordered map
recent_users = {}
recent_users[user1] = timestamp1
recent_users[user2] = timestamp2
# First user is always the first added
first_user = list(recent_users.keys())[0]
Before 3.7, you needed collections.OrderedDict. Now, regular dict does the job.
4. The Empty Tuple Singleton: () is Always ()
>>> a = ()
>>> b = ()
>>> a is b
True # Same object in memory!
>>> a = (1,)
>>> b = (1,)
>>> a is b
False # Different objects
Python has exactly one empty tuple object. Every time you write (), you get the same object.
Why? Optimization. Empty tuples are immutable and identical, so why waste memory creating multiple copies?
>>> id(())
4364568128
>>> id(())
4364568128 # Same ID every time
The same applies to small integers:
>>> a = 5
>>> b = 5
>>> a is b
True # Same object!
>>> a = 1000
>>> b = 1000
>>> a is b
False # Different objects
Python caches integers from -5 to 256. Why -5 to 256? Because that's what Guido decided. π€·
5. You Can't Actually Delete Variables (Not Really)
>>> x = [1, 2, 3]
>>> del x
>>> x
NameError: name 'x' is not defined
It looks like del deletes the variable, right? Wrong.
del removes the name, not the object.
>>> a = [1, 2, 3]
>>> b = a # b references the same list
>>> del a
>>> b
[1, 2, 3] # List still exists!
Python uses reference counting. Objects are deleted only when no references remain.
>>> import sys
>>> a = [1, 2, 3]
>>> sys.getrefcount(a)
2 # One for 'a', one for getrefcount's argument
>>> b = a
>>> sys.getrefcount(a)
3 # Now 'a' and 'b' both reference it
>>> del a
>>> sys.getrefcount(b)
2 # 'a' is gone, but object lives on
Real-world impact:
# This doesn't free memory
huge_data = load_massive_dataset()
process(huge_data)
del huge_data # Name deleted, but memory might not be freed
# If you have other references:
backup = huge_data
del huge_data # Memory NOT freed, backup still holds it
Bonus: The WTF Moment
What does this print?
>>> a = 256
>>> b = 256
>>> a is b
???
>>> a = 257
>>> b = 257
>>> a is b
???
Answer:
>>> a = 256
>>> b = 256
>>> a is b
True # Cached!
>>> a = 257
>>> b = 257
>>> a is b
False # Not cached
Remember: Python caches integers from -5 to 256. But waitβ
>>> a = 257; b = 257
>>> a is b
True # WTF?!
When you create both on the same line, the compiler optimizes them into a single object. Python is weird. π©β¨
The Takeaway
Python's built-in types are full of surprising behaviors:
- Booleans are integers
- Strings are sometimes the same object
- Dicts preserve order (now)
- Empty tuples are singletons
-
deldoesn't delete objects
Most of the time, these quirks don't matter. But when debugging a weird is comparison or a memory leak, knowing why Python behaves this way can save you hours.
Next time someone says "Python is simple," show them this article. π
What's the weirdest Python behavior you've encountered? Drop it in the comments! π
Top comments (0)