Python Data Types — The Zip Code Zero Bug
- Python's type system is not optional ceremony — the type of data determines what operations are legal, what errors you'll get, and how much memory is used. Getting the type right is getting the logic right.
- Use int for whole numbers, float for decimals, str for text — but remember that 'numbers' like zip codes and phone numbers belong as strings because you'll never do arithmetic on them.
- The four collection types have distinct personalities: list is flexible and ordered, tuple is immutable and fast, dict maps names to values, and set enforces uniqueness automatically.
- Python data types define what operations are legal and how memory is used.
- Core types: int, float, str, bool, NoneType, list, tuple, dict, set.
- Int uses ~28 bytes, float ~24 bytes, string memory grows with length.
- Wrong type choice causes silent bugs: using float for list indexing, or string for arithmetic.
- Biggest mistake: using
== Noneinstead ofis None— a custom class can break it.
Quick Debug Cheat Sheet: Type Errors
TypeError on + operator
x = "5"; y = 3; print(type(x), type(y))x = int(x) if isinstance(x, str) else xIndexError with float index
idx = some_value; print(type(idx))Check for division: result = 10 / 3 # returns 3.333AttributeError on None
def get_data(): return None; data = get_data(); print(data is None)Add guard: if data is not None: ...Production Incident
Production Debug GuideSymptom → Action guide for the most common type-related failures
type() on both operands. Likely one was meant to be a string or number. Use str() or int() to convert.// (floor division) instead of / when you need an integer result.assert var is not None before access, or propagate the None explicitly.int() with base 10: 'hello'→Validate input before conversion. Use str.isdigit() or try‑except around int() with a proper fallback.Every program you will ever write — whether it's a to-do app, a web scraper, or a machine learning model — boils down to one thing: storing and manipulating information. Before Python can store any information, it needs to understand what kind of information it is. That's not bureaucracy — it's necessity. You can add two numbers together, but you can't add a number to the word 'hello' (at least not meaningfully). Data types are the foundation that makes everything else in Python possible.
Without data types, Python would be like a chef handed a mystery ingredient with no label. Should it be grilled? Blended? Served raw? Data types give Python the context it needs to process your information correctly. They determine what operations are allowed, how much memory is used, and what the output will look like. Getting comfortable with data types early means fewer cryptic error messages and faster, more confident coding.
By the end of this article, you'll know exactly what each core Python data type is, when to reach for it, and — crucially — why the wrong choice causes bugs. You'll be able to look at any piece of data and immediately know how Python will treat it. That mental model is worth more than memorizing any syntax.
The Three Most Common Types: int, float, and str
Let's start with the data types you'll use in almost every program you ever write.
int (integer) stores whole numbers — no decimal point. Your age, a score, the number of items in a shopping cart. If it's a whole number, it's an int.
float (floating-point number) stores numbers with a decimal point. Prices, temperatures, GPS coordinates. The name 'float' comes from the way the decimal point 'floats' across the number — it can sit anywhere: 3.14, 0.001, 1000.5.
str (string) stores text — any sequence of characters wrapped in quotes. A name, an email address, an error message. The word 'string' is programmer-speak for 'a string of characters', like beads on a necklace.
Here's the important part: the type determines what operations make sense. Multiplying two ints gives you a number. Multiplying a string by an int gives you that string repeated. Python isn't confused — it's doing exactly what the types say it should. Understanding this prevents a whole class of beginner bugs before they happen.
# --- Integer (int): whole numbers with no decimal point --- player_score = 450 # int: storing a game score level_number = 7 # int: storing a level total_lives = 3 # int: storing lives remaining # --- Float: numbers that need a decimal point --- item_price = 9.99 # float: money almost always needs decimals battery_level = 87.5 # float: percentage with a decimal temperature_celsius = 36.6 # float: body temperature # --- String (str): any text, always wrapped in quotes --- player_name = "Alex" # str: a name is text, not a number error_message = "Game over!" # str: messages are always strings zip_code = "90210" # str: zip codes look like numbers but ARE text (see callout) # --- Checking the type with type() --- print(type(player_score)) # Python tells you the type of any variable print(type(item_price)) print(type(player_name)) # --- What types allow you to do --- print(player_score + level_number) # Adds two ints: 457 print(item_price * 2) # Multiplies a float: 19.98 print(player_name * 3) # Repeats a string: AlexAlexAlex # --- int + float gives you a float (Python always preserves precision) --- print(player_score + item_price) # 459.99 (not 459!)
<class 'float'>
<class 'str'>
457
19.98
AlexAlexAlex
459.99
Booleans and None — The Types That Control Logic
Once you've got numbers and text, you need a way to represent yes/no, true/false, on/off. That's where bool (boolean) comes in. A boolean can only ever be one of two values: True or False. No in-between.
Booleans are the secret engine behind every if statement you'll ever write. When you check 'is the user logged in?' or 'does this file exist?' — Python converts the answer to a boolean under the hood. Knowing this makes if statements click on a much deeper level.
None is its own special type. It represents the deliberate absence of a value — not zero, not an empty string, but genuinely nothing. Think of it like an empty form field versus a form field with a zero in it. Both look similar, but they mean very different things. You use None when a variable needs to exist but doesn't have a meaningful value yet.
These two types are smaller than int or str, but they're disproportionately powerful. Almost every conditional statement and function return value in Python touches booleans and None. Miss them and half your code becomes a mystery.
# --- Boolean (bool): only True or False, nothing else --- is_logged_in = True # bool: user authentication state has_premium_account = False # bool: subscription status is_game_over = False # bool: game state flag # --- Booleans power every if statement --- if is_logged_in: print("Welcome back!") # This runs because is_logged_in is True else: print("Please log in.") # --- Comparison operators RETURN booleans --- user_age = 17 can_vote = user_age >= 18 # This evaluates to False and stores it print(can_vote) # False print(type(can_vote)) # <class 'bool'> # --- Booleans are secretly integers in Python --- # True == 1 and False == 0 (this is intentional, not a bug) print(True + True) # 2 — useful for counting True values print(False + 5) # 5 — False adds nothing # --- None: the absence of a value --- user_middle_name = None # We don't know it yet, but the variable must exist selected_option = None # Nothing chosen yet print(user_middle_name) # None print(type(selected_option)) # <class 'NoneType'> # --- Checking for None: always use 'is', not '==' --- if user_middle_name is None: print("No middle name provided.") # This runs
False
<class 'bool'>
2
5
None
<class 'NoneType'>
No middle name provided.
if variable: instead of if variable is not None: can mask bugs when variable is 0 or empty string.s = ''; if s: is False, but the value exists. Use explicit None checks for sentinel values.is None for optional parameters and returns.is None, never == None.Collections: list, tuple, dict, and set — Storing Multiple Things at Once
So far every type has held a single value. But real programs deal with many values at once — a shopping cart full of items, a contact book full of names, a set of unique tags on a blog post. Python gives you four main collection types for this.
list is an ordered, changeable collection. The order you put items in is preserved, and you can add, remove, or change items any time. Use it when order matters and you need to modify the collection.
tuple is like a list that's been welded shut — ordered, but you can't change it after creation. Use it for data that should never change, like coordinates (lat, lng) or days of the week.
dict (dictionary) stores key-value pairs. Instead of a numbered position, each item has a named label (key). Think of it like a real dictionary: you look up the word (key) to find the definition (value). Perfect for structured data with named fields.
set is an unordered collection of unique items. Duplicates are automatically removed. Use it when you only care about what's in the collection, not how many times or in what order.
# =========================================== # LIST — ordered, changeable, allows duplicates # =========================================== shopping_cart = ["apple", "bread", "milk", "apple"] # duplicates allowed print(shopping_cart[0]) # "apple" — access by index (0 = first item) shopping_cart.append("eggs") # add an item shopping_cart.remove("bread") # remove an item print(shopping_cart) # ['apple', 'milk', 'apple', 'eggs'] # =========================================== # TUPLE — ordered, UNCHANGEABLE, allows duplicates # =========================================== gps_location = (40.7128, -74.0060) # latitude, longitude — should never change print(gps_location[0]) # 40.7128 # gps_location[0] = 99 # This would CRASH — tuples are immutable days_of_week = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") print(len(days_of_week)) # 7 # =========================================== # DICT — key-value pairs, ordered (Python 3.7+), changeable # =========================================== user_profile = { "username": "alex_codes", # key: "username", value: "alex_codes" "age": 28, # key: "age", value: 28 (an int!) "is_premium": True # values can be ANY type } print(user_profile["username"]) # "alex_codes" — access by key name user_profile["city"] = "New York" # add a new key-value pair print(user_profile) # =========================================== # SET — unordered, unique items only, no index access # =========================================== page_tags = {"python", "coding", "tutorial", "python"} # duplicate "python" is ignored print(page_tags) # {'coding', 'tutorial', 'python'} — only 3 items, order may vary # Great for checking membership fast print("python" in page_tags) # True print("java" in page_tags) # False
['apple', 'milk', 'apple', 'eggs']
40.7128
7
alex_codes
{'username': 'alex_codes', 'age': 28, 'is_premium': True, 'city': 'New York'}
{'coding', 'tutorial', 'python'}
True
False
func(list_copy=list(my_list)) or use tuple for read-only.Mutable vs Immutable Types — Why It Matters in Functions
Python types fall into two buckets: mutable (can change after creation) and immutable (cannot). This distinction matters most when you pass values into functions.
Immutable types (int, float, str, bool, tuple, NoneType, frozenset): any operation that appears to change the value actually creates a new object. Strings don't change — they get replaced. s = s + "!" creates a new string, binds it to s, and the old one gets garbage collected.
Mutable types (list, dict, set, user-defined classes): changes happen in-place. When you pass a list to a function and append to it, the caller's list changes too. That's often surprising.
This is the number one surprise for beginners when they see list changes leaking out of functions. The fix: copy before mutating, or use immutable types for data that shouldn't change.
Memory note: Immutable types can be safely shared across threads without locks. Mutable types need synchronization. This makes tuple faster than list for read-only data in concurrent contexts.
# --- Immutable: int --- x = 10 y = x x = 20 print(x, y) # 20 10 — y still holds original 10 # --- Immutable: str --- s = "hello" t = s s = s + " world" print(s, t) # hello world, hello — t unchanged # --- Mutable: list --- list_a = [1, 2] list_b = list_a list_a.append(3) print(list_a, list_b) # [1, 2, 3] [1, 2, 3] — list_b changed too! # --- The function surprise --- def append_item(item, target_list=[]): target_list.append(item) return target_list print(append_item(1)) # [1] print(append_item(2)) # [1, 2] — not [2]! print(append_item(3)) # [1, 2, 3] # Default argument is evaluated once at definition, not each call. # Fix: use None and create new list inside function.
hello world hello
[1, 2, 3] [1, 2, 3]
[1]
[1, 2]
[1, 2, 3]
None as default and create a new mutable inside the function: def f(x, lst=None): if lst is None: lst = [].Type Conversion and Checking — When Python Does It for You
Python sometimes converts types automatically (implicit conversion) and sometimes forces you to be explicit. Knowing the difference saves you from subtle bugs.
Implicit conversion: int + float → float. bool + int → int. Python widens the type to avoid losing information. You can't add str + int — that's a TypeError by design.
Explicit conversion (casting): You control when it happens. int("5") → 5, str(100) → "100", float("3.14") → 3.14. But be careful: int("hello") raises ValueError.
Type checking: Use isinstance() over type() in production. isinstance handles inheritance correctly, while type() checks exact type. For example: isinstance(True, int) is True, but type(True) == int is False (because bool is a subclass of int).
Duck typing: 'If it walks like a duck and quacks like a duck, it's a duck.' Python doesn't care about the actual type — only that the object has the methods you need. But this can lead to runtime errors if the object lacks the method. Type hints and static checkers like mypy help catch these early.
# --- Implicit conversion --- result = 5 + 3.2 # 8.2 — int promoted to float count = True + 2 # 3 — bool promoted to int print(result, count) # --- Explicit conversion with try/except --- def safe_int(value): try: return int(value) except (ValueError, TypeError): return None # or handle gracefully print(safe_int("42")) # 42 print(safe_int("hello")) # None print(safe_int(3.14)) # 3 (truncates decimal) # --- isinstance vs type --- class MyInt(int): pass num = MyInt(10) print(type(num) == int) # False — exact type check fails print(isinstance(num, int)) # True — isinstance works # --- Duck typing example --- def double(x): return x * 2 print(double(5)) # 10 (int) print(double("Hi")) # "HiHi" (str) — both work! But what about a custom object?
3
42
None
3
False
True
10
HiHi
append() on a tuple, you get AttributeError. Use type hints and mypy for large codebases — they catch these mismatches at check time, not runtime.decimal.Decimal type and convert explicitly.isinstance() not type() for flexible checks.| Feature | int | float | str | bool | NoneType | list | tuple | dict | set |
|---|---|---|---|---|---|---|---|---|---|
| Mutability | Immutable | Immutable | Immutable | Immutable | Immutable | Mutable | Immutable | Mutable | Mutable |
| Ordered | N/A | N/A | N/A | N/A | N/A | Yes | Yes | Yes (3.7+) | No |
| Duplicates | N/A | N/A | N/A | N/A | N/A | Yes | Yes | Keys: No, Values: Yes | No |
| Memory (approx) | 28 bytes | 24 bytes | 49 bytes + 1 per char | 28 bytes | 16 bytes | 56 bytes + 8 per element | 56 bytes + 8 per element | 240 bytes + 8 per entry | 224 bytes + 8 per element |
| Best used for | Counts, indices, IDs | Measurements, percentages | Text, messages, codes | Flags, conditions | Absence of value | Sequence that changes | Fixed data (coordinates) | Structured records | Unique items, membership |
🎯 Key Takeaways
- Python's type system is not optional ceremony — the type of data determines what operations are legal, what errors you'll get, and how much memory is used. Getting the type right is getting the logic right.
- Use int for whole numbers, float for decimals, str for text — but remember that 'numbers' like zip codes and phone numbers belong as strings because you'll never do arithmetic on them.
- The four collection types have distinct personalities: list is flexible and ordered, tuple is immutable and fast, dict maps names to values, and set enforces uniqueness automatically.
- None is not zero, not False, and not an empty string — it is the deliberate absence of a value. Always check for it with
is None, never with== None. - Mutable default arguments in functions are a common source of bugs — use None and initialize inside the function.
- Use
for type checking in production, notisinstance(). It handles subclass relationships correctly.type()
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between a list and a tuple in Python, and when would you deliberately choose a tuple over a list?JuniorReveal
- QIs bool a subclass of int in Python? What does True + True evaluate to, and why?Mid-levelReveal
- QWhat happens when you use a list as a key in a Python dictionary, and why does Python raise a TypeError? What types can be used as dictionary keys?Mid-levelReveal
- QExplain the concept of type erasure in Python generics. How does type hinting affect runtime?SeniorReveal
Frequently Asked Questions
How many data types does Python have?
Python has several built-in data types. The most commonly used ones are int, float, str, bool, NoneType, list, tuple, dict, and set. There are others (like complex numbers, bytes, and frozenset), but mastering these nine will cover the vast majority of real-world Python programming you'll encounter.
How do I check the data type of a variable in Python?
Use the built-in function. For example, type()type(42) returns <class 'int'> and type("hello") returns <class 'str'>. If you need to check the type inside an if statement (like validating user input), use instead — for example, isinstance()isinstance(42, int) returns True. is preferred in production code because it also handles subclasses correctly.isinstance()
Can a variable in Python change its data type?
Yes — and this is one of Python's defining features called dynamic typing. Unlike Java or C++, you don't declare a type upfront. A variable like score = 10 holds an int, but you can later write score = 'ten' and it becomes a string. Python doesn't complain. This flexibility is powerful but requires discipline — changing a variable's type mid-program is a common source of bugs that are hard to track down, so do it intentionally or not at all.
What is the difference between `is` and `==` when checking None?
is checks object identity (whether two references point to the same object in memory), while == checks value equality. None is a singleton — there is only one None object. So x is None is always correct and faster. x == None can return True even if x is not None if a custom class overrides __eq__. Always use is None and is not None.
Why does using a mutable default argument cause issues?
Default arguments in Python are evaluated once at function definition time, not every time the function is called. So if the default is a mutable object like a list, that same list object is reused across all calls. Any mutation (like append) will persist across calls. The fix is to use None as the default and create a new mutable inside the function: def f(x, lst=None): if lst is None: lst = [].
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.