DEV Community

Cover image for Python Types Are Weirder Than You Think 🐍
Razumovsky
Razumovsky

Posted on

Python Types Are Weirder Than You Think 🐍

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Bonus: The WTF Moment

What does this print?

>>> a = 256
>>> b = 256
>>> a is b
???

>>> a = 257
>>> b = 257
>>> a is b
???




Enter fullscreen mode Exit fullscreen mode

Answer:

>>> a = 256
>>> b = 256
>>> a is b
True  # Cached!

>>> a = 257
>>> b = 257
>>> a is b
False  # Not cached
Enter fullscreen mode Exit fullscreen mode

Remember: Python caches integers from -5 to 256. But waitβ€”

>>> a = 257; b = 257
>>> a is b
True  # WTF?!
Enter fullscreen mode Exit fullscreen mode

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
  • del doesn'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)