๐Ÿงฉ Data Types

Python Data Types โ€” The Complete Guide

What every built-in type actually is, why Python offers more than one way to group values together, and how to pick the right one instead of reaching for whatever's familiar.

๐Ÿ“…

Last Updated

April 2026

โฑ๏ธ

Read Time

40 min

๐ŸŽฏ

Level

Beginner

What Do We Actually Mean by a 'Data Type'?

Every single value a program works with โ€” the number 7, the word "hello", the answer true or false โ€” needs to be stored in memory in some particular format, and that format determines what operations are actually valid on it. You can add two numbers together, but adding two words the same way makes no sense unless the language decides to interpret that as joining them. A data type is simply the category a value belongs to, and it's this category that tells Python (and you) what can and can't be reasonably done with it.

In Python specifically, every value you ever create is an instance of some type, and you can always ask Python directly what type a value is using the built-in type() function. This single habit โ€” checking type() when something behaves oddly โ€” probably resolves more beginner confusion than any other single debugging trick, because a surprising number of 'weird' bugs in early Python code trace back to a value quietly being a different type than the person writing the code assumed.

๐Ÿ Pythonchecking_types.py
print(type(7))          # <class 'int'>
print(type(7.5))        # <class 'float'>
print(type("hello"))    # <class 'str'>
print(type(True))       # <class 'bool'>
print(type([1, 2, 3]))  # <class 'list'>
print(type((1, 2)))     # <class 'tuple'>
print(type({1, 2}))     # <class 'set'>
print(type({"a": 1}))   # <class 'dict'>
print(type(None))       # <class 'NoneType'>

Python's built-in types are usually split into a few broad families for teaching purposes: numeric types (integers, floats, complex numbers), text (strings), sequence types that hold ordered collections of items (lists, tuples, ranges), a mapping type (dictionaries), set types (sets and frozensets), a boolean type, and the singular None type that represents the absence of a value. None of this categorisation is set in stone anywhere in the language itself โ€” it's a convenient way for humans to organise the discussion โ€” but it's the grouping most tutorials, including this one, follow, since it roughly mirrors how you'll actually reach for these types while writing real code.

Numeric Types โ€” int, float, and complex

Python offers three built-in numeric types, and in day-to-day programming you'll spend the overwhelming majority of your time with just the first two.

๐Ÿ Pythonnumeric_types.py
whole_number = 42          # int
decimal_number = 3.14      # float
imaginary = 2 + 3j         # complex

print(type(whole_number))   # <class 'int'>
print(type(decimal_number)) # <class 'float'>
print(type(imaginary))      # <class 'complex'>

An int represents a whole number, positive or negative, with no fractional part. One detail that genuinely sets Python apart from most other mainstream languages is that Python integers have no fixed size limit โ€” you can work with a number that's hundreds of digits long, and Python will handle it correctly without overflowing, automatically allocating more memory as needed behind the scenes. Languages like Java or C, by contrast, typically restrict a standard integer to a fixed number of bits, meaning very large numbers require special handling to avoid silently wrapping around to an incorrect value.

๐Ÿ Pythonbig_integers.py
huge_number = 2 ** 100
print(huge_number)
# 1267650600228229401496703205376

print(type(huge_number))  # still just <class 'int'> -- no special 'long' type needed

A float represents a number with a decimal point, and internally Python stores floats using the IEEE 754 double-precision standard, the same format most programming languages use for decimal numbers. This is worth knowing because it explains a genuinely common source of confusion for newcomers: floating-point numbers can't always represent decimal fractions with perfect precision, since they're stored in binary rather than base-10.

๐Ÿ Pythonfloat_precision.py
print(0.1 + 0.2)
# 0.30000000000000004 -- not a bug, just how binary floating-point works

print(round(0.1 + 0.2, 2))
# 0.3 -- rounding for display or comparison sidesteps the issue

This isn't a flaw specific to Python โ€” it happens in essentially every language using the same underlying floating-point standard โ€” but it catches enough beginners off guard that it's worth calling out directly the first time floats are introduced. For situations where exact decimal precision genuinely matters, such as handling money, Python's standard library includes a decimal module specifically designed to avoid this issue, though for most everyday calculations, ordinary floats are perfectly fine.

The third numeric type, complex, represents complex numbers with a real and an imaginary part, written using a trailing j for the imaginary component. Most beginners won't touch this type for a long while โ€” it shows up primarily in specialised scientific, engineering, and signal-processing contexts โ€” but it's built directly into the language rather than requiring a separate library, which says something about how seriously Python takes numerical computing as a use case.

Working Across Numeric Types

Python allows arithmetic directly between an int and a float in the same expression, automatically converting the result to whichever type carries more information โ€” in practice, that means mixing an int with a float always produces a float, since a float can represent everything an int can, but not the other way around.

๐Ÿ Pythonimplicit_numeric_conversion.py
result = 5 + 2.5
print(result)        # 7.5
print(type(result))  # <class 'float'>

# Division always returns a float in Python 3, even for two integers
print(10 / 2)        # 5.0, not 5
print(type(10 / 2))  # <class 'float'>

# Floor division keeps things as an int when both operands are ints
print(10 // 2)       # 5
print(type(10 // 2)) # <class 'int'>

That behaviour around division is a genuine, deliberate difference between Python 2 and Python 3. In Python 2, dividing two integers using / performed integer division by default, silently discarding any remainder. Python 3 changed / to always perform true division, returning a float, and introduced // specifically for floor division โ€” a change made precisely because the old behaviour was a common, silent source of bugs, especially for anyone coming from a mathematical or scientific background where division simply means division.

range โ€” A Lazy Sequence of Numbers

Before moving on to text, it's worth introducing one more sequence type that's easy to overlook precisely because it so often appears tucked inside a for loop rather than being assigned to a variable directly: range. A range represents a sequence of numbers, and it's what's most commonly used to control how many times a loop repeats.

๐Ÿ Pythonrange_basics.py
numbers = range(5)
print(numbers)          # range(0, 5) -- not a list of numbers printed out directly
print(list(numbers))    # [0, 1, 2, 3, 4]

for i in range(2, 10, 2):
    print(i, end=" ")
# 2 4 6 8

The detail worth genuinely understanding here, rather than just memorising, is that range() does not actually build and store a full list of numbers in memory. It's what's called a lazy sequence โ€” it calculates each number on demand, one at a time, as something asks for it, rather than generating and holding onto the whole sequence up front. This is why printing a range directly shows something like range(0, 5) rather than the expanded numbers themselves; you have to explicitly convert it to a list, using list(), to see every value materialised at once.

This laziness matters a great deal in practice once numbers get large. Looping over range(10_000_000) costs almost no memory at all, since Python never needs to hold ten million numbers in memory simultaneously โ€” it just produces the next one, uses it, and discards it. Building the equivalent list explicitly with list(range(10_000_000)), on the other hand, would genuinely allocate memory for every single number at once, which is a meaningfully different, heavier operation. This same idea of computing values lazily, on demand, resurfaces later in more advanced Python through generators, and range is a gentle, built-in first taste of that pattern.

Strings โ€” Python's Text Type

A str (short for 'string') represents a sequence of characters โ€” words, sentences, or really any text at all. Strings can be written using single quotes, double quotes, or triple quotes, and Python treats all three as functionally identical for ordinary strings, the choice generally coming down to convenience or house style.

๐Ÿ Pythonstring_basics.py
greeting = 'Hello'
name = "Priya"
bio = """A multi-line string,
useful for longer blocks of text
spanning several lines."""

print(greeting + ", " + name + "!")  # Hello, Priya!
print(len(name))                       # 5

One property of strings that surprises a lot of newcomers is that they are immutable โ€” once a string object is created, it can never be changed in place. Every operation that looks like it's 'modifying' a string is actually building a brand-new string object and handing that back to you.

๐Ÿ Pythonstring_immutability.py
word = "cat"
# word[0] = "b"   # TypeError: 'str' object does not support item assignment

# The correct approach is to build a new string entirely
word = "b" + word[1:]
print(word)  # bat

Strings support indexing and slicing much like lists do, since a string is really just an ordered sequence of individual characters underneath. Indexing starts at 0, and negative indices count backward from the end, a convention that applies consistently across every ordered sequence type in Python, not just strings.

๐Ÿ Pythonstring_indexing_slicing.py
text = "Python"

print(text[0])     # P
print(text[-1])    # n -- last character
print(text[0:3])   # Pyt -- characters at index 0, 1, 2
print(text[2:])    # thon -- from index 2 to the end
print(text[::-1])  # nohtyP -- a common trick to reverse a string

Modern Python also gives you f-strings (formatted string literals, introduced in Python 3.6), which are by far the most readable and idiomatic way to build a string that embeds variable values. You'll see these everywhere in current Python code, and for good reason โ€” they remove a lot of the awkward concatenation and formatting syntax older approaches required.

๐Ÿ Pythonf_strings.py
product = "Notebook"
price = 149.5
quantity = 3

print(f"{quantity} x {product} = โ‚น{price * quantity:.2f}")
# 3 x Notebook = โ‚น448.50

Unicode Strings and Encoding โ€” Why Python 3 Handles Text Differently

One of the quieter but genuinely significant changes between Python 2 and Python 3 involves exactly how text is stored underneath. In Python 3, every ordinary string is Unicode by default, capable of representing characters from virtually any written language, along with emoji and a huge range of other symbols, without needing any special setup or import.

๐Ÿ Pythonunicode_strings.py
greeting_hindi = "เคจเคฎเคธเฅเคคเฅ‡"
greeting_japanese = "ใ“ใ‚“ใซใกใฏ"
celebration = "Congratulations! ๐ŸŽ‰"

print(greeting_hindi, greeting_japanese, celebration)
print(len(greeting_hindi))  # 6 -- counts characters, not raw bytes

This matters because, underneath, a computer only ever stores raw bytes โ€” a string has to be converted, or encoded, into a specific sequence of bytes before it can be saved to a file or sent across a network, and converted back, or decoded, to be read as text again. Python 3 keeps this distinction explicit and clean: an ordinary str is Unicode text, and a separate type, bytes, represents the raw, encoded form. The two are related but genuinely different types, and Python won't silently mix them together.

๐Ÿ Pythonencoding_decoding.py
text = "cafรฉ"
encoded = text.encode("utf-8")
print(encoded)          # b'caf\xc3\xa9' -- a bytes object, not a string
print(type(encoded))    # <class 'bytes'>

decoded = encoded.decode("utf-8")
print(decoded)          # cafรฉ -- back to an ordinary string
print(type(decoded))    # <class 'str'>

In Python 2, by contrast, an ordinary string was really just a sequence of raw bytes by default, and true Unicode text needed a separate, explicitly marked type. This led to a genuine, recurring category of bugs when a program mixed encoded bytes and Unicode text without realising it, since Python 2 would often try to silently guess how to combine them, sometimes getting it wrong in ways that only surfaced with certain non-English characters. Python 3's stricter separation between str and bytes was one of the more consequential, if less flashy, reasons the language's core maintainers considered the backward-incompatible break worth making.

Booleans โ€” True, False, and What Counts as 'Truthy'

The bool type has exactly two possible values: True and False, written with a capital first letter, unlike some other languages where they're written entirely in lowercase. Booleans are what conditions in an if statement ultimately evaluate down to, and they're what comparison operators like ==, <, and in return.

A detail worth knowing early: bool is technically a subtype of int in Python, which is a slightly unusual design choice compared to most languages, but a genuinely deliberate one. Because of this relationship, True behaves exactly like 1 in arithmetic, and False behaves exactly like 0.

๐Ÿ Pythonbool_as_int.py
print(True + True)   # 2
print(False * 10)    # 0
print(isinstance(True, int))  # True -- bool really is a subtype of int
print(True == 1)     # True
print(False == 0)    # True

Beyond actual booleans, Python has a broader concept called truthiness, where every object โ€” not just True and False โ€” can be evaluated in a boolean context, like an if statement. A handful of specific values are treated as 'falsy', meaning they behave like False when checked this way, and everything else is treated as 'truthy'.

ValueTreated AsCategory
0, 0.0FalsyNumeric zero, in any numeric type
"" (empty string)FalsyAny empty sequence
[] (empty list)FalsyAny empty sequence
{} (empty dict)FalsyAny empty mapping
set() (empty set)FalsyAny empty set
NoneFalsyThe absence of a value
Any non-zero numberTruthye.g. 1, -1, 3.14
Any non-empty sequence or collectionTruthye.g. "hi", [0], {0: 0}
๐Ÿ Pythontruthiness_demo.py
items = []

if items:
    print("There are items in the list.")
else:
    print("The list is empty.")  # this runs, since an empty list is falsy

# This is generally preferred over the more verbose:
# if len(items) == 0:

Relying on truthiness this way โ€” writing if items: instead of if len(items) == 0: โ€” is considered idiomatic, readable Python, and you'll see experienced developers do it constantly. It's worth learning to read comfortably even though it can feel slightly unintuitive the first time you encounter it, since an empty list isn't literally the value False, it just behaves like it in this specific context.

Lists โ€” Ordered, Mutable Collections

A list is an ordered collection of items, written between square brackets and separated by commas. Lists are probably the single most commonly used data structure in everyday Python code, and for good reason: they're flexible, they preserve the order items were added in, and โ€” crucially โ€” they're mutable, meaning you can change their contents after creation without having to build a whole new object.

๐Ÿ Pythonlist_basics.py
fruits = ["apple", "banana", "mango"]

fruits.append("grape")       # add to the end
fruits[1] = "blueberry"       # replace an existing item by index
fruits.remove("apple")        # remove a specific item by value

print(fruits)  # ['blueberry', 'mango', 'grape']
print(len(fruits))  # 3

A single list can genuinely hold a mix of different types at once โ€” an integer, a string, and even another list can all sit side by side in the same list โ€” because Python lists don't enforce any type consistency among their elements. This is quite different from arrays in more strictly typed languages, which usually require every element to share exactly one declared type.

๐Ÿ Pythonmixed_type_list.py
mixed = [1, "two", 3.0, [4, 5], True]
print(mixed)
print(type(mixed[3]))  # <class 'list'> -- a list nested inside a list

Lists support the same indexing and slicing syntax already introduced for strings, since both are sequence types underneath. They also support a particularly popular and expressive shorthand called a list comprehension, which builds a new list from an existing iterable in a single, compact line.

๐Ÿ Pythonlist_comprehension.py
numbers = [1, 2, 3, 4, 5, 6]

even_squares = [n * n for n in numbers if n % 2 == 0]
print(even_squares)  # [4, 16, 36]

# Equivalent, longer version using a plain loop:
even_squares_manual = []
for n in numbers:
    if n % 2 == 0:
        even_squares_manual.append(n * n)

Tuples โ€” Ordered, Immutable Collections

A tuple looks almost identical to a list at first glance โ€” an ordered collection of items โ€” but it's written with parentheses instead of square brackets, and, critically, it is immutable. Once a tuple is created, you cannot add, remove, or change any of its elements.

๐Ÿ Pythontuple_basics.py
coordinates = (28.6, 77.2)

# coordinates[0] = 29.0  # TypeError: 'tuple' object does not support item assignment

print(coordinates)      # (28.6, 77.2)
print(coordinates[1])   # 77.2

It's fair to ask why Python bothers offering both a mutable and an immutable version of essentially the same idea, and the answer comes down to intent and, in some situations, genuine technical necessity. A tuple communicates to anyone reading the code that this particular grouping of values is fixed and shouldn't be expected to change โ€” a pair of coordinates, the red-green-blue components of a colour, or a row returned from a database query are all natural fits for a tuple, since their structure and meaning are complete the moment they're created.

There's also a practical technical reason: because tuples are immutable, they can be used as keys in a dictionary or as elements inside a set, both of which require their contents to be hashable โ€” a property that mutable types like lists simply don't have, precisely because their contents (and therefore what would need to be hashed) can change after creation.

๐Ÿ Pythontuple_as_dict_key.py
distances = {
    ("Delhi", "Mumbai"): 1400,
    ("Delhi", "Chennai"): 2200
}

print(distances[("Delhi", "Mumbai")])  # 1400

# The same thing with a list key would fail immediately:
# bad = {["Delhi", "Mumbai"]: 1400}  # TypeError: unhashable type: 'list'

A small syntax quirk worth remembering: a tuple containing exactly one item requires a trailing comma to actually be recognised as a tuple, since parentheses alone are also used for ordinary grouping in mathematical expressions.

๐Ÿ Pythonsingle_item_tuple.py
not_a_tuple = (5)
print(type(not_a_tuple))   # <class 'int'> -- just a plain integer in parentheses

actual_tuple = (5,)
print(type(actual_tuple))  # <class 'tuple'>

Sets โ€” Unordered Collections of Unique Items

A set is a collection that automatically enforces two rules: every item inside it must be unique, and the collection has no fixed, meaningful order. Sets are written using curly braces, and they're the natural choice any time your actual problem is really about membership and uniqueness rather than order or position.

๐Ÿ Pythonset_basics.py
tags = {"python", "tutorial", "beginner", "python"}
print(tags)  # {'python', 'tutorial', 'beginner'} -- the duplicate is dropped automatically

tags.add("guide")
tags.discard("beginner")
print(tags)

print("python" in tags)  # True -- membership checks on a set are extremely fast

Sets really shine when you need to compare collections against each other, since Python implements the standard mathematical set operations directly, and they read almost exactly like the underlying concept they represent.

๐Ÿ Pythonset_operations.py
python_devs = {"Aman", "Riya", "Zaid", "Neha"}
java_devs = {"Riya", "Sameer", "Neha", "Tarun"}

print(python_devs & java_devs)  # {'Riya', 'Neha'} -- intersection, known by both
print(python_devs | java_devs)  # union, everyone across both groups
print(python_devs - java_devs)  # difference, python-only devs

One frequent, practical use of sets is quietly removing duplicate entries from an existing list, which turns out to be a single, extremely short line thanks to how easily lists and sets convert into each other.

๐Ÿ Pythonremoving_duplicates.py
numbers = [4, 2, 4, 6, 2, 8, 6]
unique_numbers = list(set(numbers))
print(unique_numbers)  # [8, 2, 4, 6] -- order is not guaranteed to be preserved

Note the caveat in that last example โ€” converting through a set doesn't preserve the original order the numbers appeared in, since sets simply don't track any order at all. If preserving the original order while still removing duplicates genuinely matters, a slightly different approach using a dictionary (covered next) is usually the better tool for the job, since dictionaries in modern Python do reliably preserve insertion order.

frozenset โ€” The Immutable Sibling of set

Just as tuples exist alongside lists to offer an immutable version of an ordered collection, Python offers frozenset as the immutable counterpart to an ordinary set. Once created, a frozenset's contents can never be added to or removed โ€” it behaves exactly like a set for reading and comparing, but it locks the collection in place.

๐Ÿ Pythonfrozenset_demo.py
allowed_roles = frozenset({"admin", "editor", "viewer"})

print("editor" in allowed_roles)  # True
# allowed_roles.add("guest")  # AttributeError: 'frozenset' object has no attribute 'add'

print(type(allowed_roles))  # <class 'frozenset'>

The practical reason to reach for a frozenset instead of an ordinary set is exactly the same reason you'd reach for a tuple instead of a list: because it's immutable, a frozenset is hashable, and can therefore be used as a dictionary key or nested inside another set, neither of which an ordinary, mutable set supports. It shows up far less often than the other collection types covered in this guide, but it's worth recognising by name, since it does appear in code dealing with fixed configuration values or permission groupings that are deliberately meant to stay constant.

Dictionaries โ€” Key-Value Mappings

A dict stores data as pairs of keys and values, letting you look up a value quickly using a meaningful key instead of a numeric position. Dictionaries are written with curly braces, using a colon to separate each key from its corresponding value.

๐Ÿ Pythondict_basics.py
student = {
    "name": "Devika",
    "age": 21,
    "course": "Computer Science"
}

print(student["name"])    # Devika
student["age"] = 22        # update an existing value
student["college"] = "IIT"  # add a brand new key

print(student)

Dictionary keys have a specific requirement worth knowing: a key must be a hashable, immutable type โ€” strings, numbers, and tuples all work fine as keys, but a list or another dictionary cannot be used as a key, for the same reason lists can't be placed inside a set. Values, on the other hand, have no such restriction at all; a dictionary value can be absolutely anything, including another list or dictionary.

A genuinely important, somewhat lesser-known fact worth calling out clearly: since Python 3.7, dictionaries officially guarantee that they preserve insertion order โ€” the order keys were originally added is the order you'll get back when iterating over the dictionary. Before that version, this ordering happened to occur as an implementation detail in one particular Python version but wasn't a guaranteed part of the language's actual specification, so relying on it in code meant to run on older versions wasn't safe.

๐Ÿ Pythondict_iteration.py
scores = {"maths": 88, "science": 92, "history": 79}

for subject, mark in scores.items():
    print(subject, "->", mark)

# maths -> 88
# science -> 92
# history -> 79 -- guaranteed to appear in exactly this order

Dictionaries are, by a wide margin, one of the most heavily used data structures across real Python code, since so many real-world problems are naturally described as 'look this value up by that name' โ€” configuration settings, JSON data from a web API, counting how often each word appears in a piece of text, and countless other everyday tasks all map cleanly onto a dictionary.

NoneType โ€” Representing the Absence of a Value

None is a genuinely unique value in Python โ€” it's the sole instance of its own type, NoneType, and it exists specifically to represent 'no meaningful value here', as distinct from an empty string, a zero, or an empty list, each of which still represents a real, present value that simply happens to be empty or zero.

๐Ÿ Pythonnone_type_demo.py
result = None
print(type(result))  # <class 'NoneType'>

def find_discount(code):
    discounts = {"SAVE10": 10, "SAVE20": 20}
    return discounts.get(code)  # returns None automatically if the code isn't found

print(find_discount("SAVE10"))   # 10
print(find_discount("INVALID"))  # None

You'll run into None constantly, whether you go looking for it or not โ€” it's what a function returns automatically if it doesn't hit an explicit return statement, it's what many dictionary and search methods return when a lookup fails to find a match, and it's a natural, sensible default value for a function parameter that's genuinely optional.

Mutable vs Immutable โ€” The Distinction That Ties Everything Together

By this point in the guide, one property keeps resurfacing across almost every type discussed: whether a value can be changed in place after it's created, or whether every 'change' actually produces an entirely new object. This distinction โ€” mutability โ€” is arguably the single most important organising idea across Python's built-in data types, and it's worth summarising clearly in one place rather than leaving it scattered.

TypeMutable?Can Be a Dict Key / Set Element?
int, float, complexNoYes
strNoYes
boolNoYes
tupleNo (but can hold mutable items inside it)Yes, if all its contents are also hashable
listYesNo
dictYesNo
setYesNo
frozensetNoYes
NoneN/A โ€” a single, unique valueYes

This table is genuinely worth returning to whenever you're unsure why a particular piece of code raised an error, especially anything involving TypeError: unhashable type, which almost always traces directly back to trying to use a mutable type somewhere Python specifically requires an immutable, hashable one.

It's worth noting one subtlety that trips people up: a tuple is technically immutable itself, but if it contains a mutable object โ€” a list, say โ€” that inner list can still be modified, even though the tuple's own structure (which items it holds, and in what order) can't be. Because of this, a tuple containing a list is not actually hashable, and can't be used as a dictionary key or set element, even though tuples generally can be.

๐Ÿ Pythontuple_with_mutable_content.py
mixed_tuple = (1, 2, [3, 4])
mixed_tuple[2].append(5)   # this works fine -- the inner list is still mutable
print(mixed_tuple)          # (1, 2, [3, 4, 5])

# mixed_tuple[0] = 99       # this still fails -- the tuple's own structure is fixed

# hash(mixed_tuple)         # TypeError: unhashable type: 'list'
# because one of its elements is itself unhashable

Type Conversion โ€” Moving Between Types Deliberately

Because Python is dynamically but strongly typed, it won't silently guess how to combine incompatible types on your behalf โ€” but it does provide clear, explicit functions for converting a value from one type to another whenever that's genuinely what you want. This is usually called type conversion or type casting, and it's an everyday, routine part of working with Python, not an advanced or unusual technique.

๐Ÿ Pythonexplicit_type_conversion.py
age_text = "25"
age_number = int(age_text)
print(age_number + 5)  # 30

price = 149
price_text = str(price)
print("Price: " + price_text)  # Price: 149

print(float("3.14"))   # 3.14
print(int(3.99))       # 3 -- truncates the decimal, does not round
print(bool(0))         # False
print(bool("no"))      # True -- any non-empty string is truthy, regardless of its content

That last line is worth pausing on specifically, since it catches out a genuine number of beginners: bool("no") evaluates to True, not False, even though the literal text says 'no'. Python's bool() conversion has no understanding of English words at all โ€” it purely checks whether the string is empty or not, and since "no" is a non-empty string, it counts as truthy, regardless of what the text actually says.

Converting between collection types is just as common, especially the conversions already touched on earlier in this guide โ€” turning a list into a set to remove duplicates, or turning a dictionary's keys or values into a list to work with them positionally.

๐Ÿ Pythoncollection_conversion.py
letters_list = ["a", "b", "a", "c"]
letters_set = set(letters_list)      # list -> set
letters_tuple = tuple(letters_list)  # list -> tuple

scores = {"a": 90, "b": 85}
keys_list = list(scores.keys())      # dict keys -> list
values_list = list(scores.values())  # dict values -> list

print(letters_set, letters_tuple, keys_list, values_list)

One conversion that regularly trips people up is trying to convert a string that isn't actually a valid number, which raises a ValueError rather than silently returning something like zero or None. This is entirely consistent with Python's strong typing philosophy โ€” the language would rather fail loudly and immediately than guess at what you probably meant.

๐Ÿ Pythonconversion_error.py
user_input = "twenty"
# age = int(user_input)  # ValueError: invalid literal for int() with base 10: 'twenty'

# Handling this safely in real code:
try:
    age = int(user_input)
except ValueError:
    print("That doesn't look like a valid number.")

Choosing the Right Data Type โ€” A Practical Decision Guide

Knowing what each type is capable of is only half the picture โ€” the more practically useful skill is developing a quick sense of which type actually fits a given problem. This is a judgment call experienced Python developers make almost automatically, but it's worth spelling out explicitly for anyone still building that instinct.

  • โ–ถ

    Need an ordered collection you'll add to, remove from, or rearrange? Reach for a list.

  • โ–ถ

    Need a fixed grouping of values that shouldn't change once created, like a coordinate pair or an RGB colour? Reach for a tuple.

  • โ–ถ

    Need to check membership quickly, or guarantee every item is unique? Reach for a set.

  • โ–ถ

    Need to look values up by a meaningful name rather than a numeric position? Reach for a dictionary.

  • โ–ถ

    Working with text of any kind โ€” a name, a sentence, a file path? That's a string, almost by definition.

  • โ–ถ

    Need a value that's naturally on/off, yes/no, or a condition's outcome? Use a boolean.

  • โ–ถ

    Need to represent 'nothing here yet' or 'no match found', distinct from zero or an empty collection? Use None.

A genuinely common beginner mistake is reaching for a list purely out of habit, even in situations where a set or a dictionary would represent the actual problem far more naturally. If you find yourself writing code that repeatedly loops through a list just to check whether something is already inside it, that's usually a strong signal a set would fit the job better and run considerably faster, since checking membership in a set is far quicker than scanning through a list item by item.

Sequences, Mappings, and Sets โ€” The Bigger Picture

Zooming out slightly, Python's collection types can be grouped into three broader families, and understanding which family a type belongs to explains a lot about how it behaves generally, beyond just the specific type itself.

FamilyDefining TraitMembersAccess Style
SequencesOrdered, accessed by numeric positionstr, list, tuple, rangeIndexing and slicing, e.g. my_seq[0], my_seq[1:3]
MappingsAccessed by an arbitrary, meaningful keydictKey lookup, e.g. my_dict["key"]
SetsUnordered, guarantees uniquenessset, frozensetMembership checks and set algebra, e.g. item in my_set

This grouping is exactly why the same slicing syntax works identically on strings, lists, and tuples โ€” they're all sequences, so Python applies the same consistent access rules to each of them, rather than inventing a separate syntax for every individual type. Once this pattern clicks, learning any new sequence-like type in Python (and there are several more in the standard library beyond the built-in basics) becomes considerably faster, since you already know roughly what to expect from it.

Does the Data Type You Choose Actually Affect Performance?

Yes, often more noticeably than beginners expect, and it's worth understanding why in practical terms rather than treating it as an abstract computer-science concern. The clearest example is checking whether a value exists inside a collection. Doing this against a list requires Python to potentially examine every single item, one after another, until it either finds a match or reaches the end โ€” the more items in the list, the longer this can take. Doing the exact same check against a set or a dictionary's keys is, by contrast, close to instantaneous regardless of how large the collection grows, because both rely on a hashing mechanism that jumps almost directly to where a matching item would be, rather than scanning through everything.

๐Ÿ Pythonmembership_check_comparison.py
import time

big_list = list(range(1_000_000))
big_set = set(big_list)

start = time.time()
999_999 in big_list  # has to scan through the list
print("List check:", time.time() - start)

start = time.time()
999_999 in big_set   # jumps almost straight there
print("Set check:", time.time() - start)

Running something like this on your own machine typically shows the set-based check completing many times faster than the list-based one, and the gap only widens as the collection grows larger. This is exactly why a common piece of practical advice for working Python developers is: if your code repeatedly checks whether something is 'in' a collection, and that collection doesn't need to preserve duplicates or a specific order, converting it to a set is often the single easiest performance improvement available, requiring barely any change to the surrounding logic.

None of this means you should obsess over performance from the very first line of code you write โ€” for small collections, the difference is genuinely too small to notice or care about, and readability should usually win by default. But it's worth having this trade-off in the back of your mind once your programs start working with larger amounts of data, since choosing the right built-in type for the job is very often a simpler, more maintainable win than reaching for a specialised optimisation technique later.

Common Mistakes Involving Data Types

A handful of type-related mistakes come up often enough among beginners โ€” and honestly, sometimes among experienced developers too, on an off day โ€” that they're worth flagging directly.

  • โ–ถ

    Assuming division always returns an int โ€” in Python 3, the ordinary / operator always returns a float, even when dividing two whole numbers evenly. Use // specifically when an int result is genuinely required.

  • โ–ถ

    Comparing floats for exact equality โ€” due to how floating-point numbers are stored in binary, a calculation like 0.1 + 0.2 == 0.3 evaluates to False. Comparing floats within a small tolerance, or rounding first, avoids this trap.

  • โ–ถ

    Trying to modify a string or tuple in place โ€” both are immutable, so any attempt to change an individual element directly raises a TypeError; a new object must be created instead.

  • โ–ถ

    Using a mutable type like a list as a dictionary key โ€” this raises TypeError: unhashable type immediately, since dictionary keys must be hashable, and lists never are.

  • โ–ถ

    Assuming a set preserves the order items were added in โ€” sets have no guaranteed order at all; if order matters, a list or, more recently, an ordinary dictionary (used just for its keys) is the appropriate choice instead.

  • โ–ถ

    Forgetting the trailing comma when writing a single-item tuple โ€” (5) is simply the integer 5 in parentheses, not a tuple; the correct syntax is (5,).

  • โ–ถ

    Assuming bool() understands the meaning of a string's text โ€” bool("False") evaluates to True, since it's checking only whether the string is empty, not interpreting its content.

Checking Types Correctly in Real Code

Once you start writing functions meant to handle input from other parts of a program โ€” or from an actual user โ€” it becomes useful to check a value's type deliberately, rather than assuming it's always what you expect. Python offers two main tools for this, and the difference between them matters in practice.

๐Ÿ Pythonisinstance_vs_type.py
class Animal:
    pass

class Dog(Animal):
    pass

my_pet = Dog()

print(type(my_pet) == Dog)          # True
print(type(my_pet) == Animal)       # False -- type() checks the EXACT class only
print(isinstance(my_pet, Animal))   # True -- isinstance() also recognises inheritance
print(isinstance(my_pet, Dog))      # True

In practice, isinstance() is almost always the better tool for genuine type checks inside real program logic, precisely because it correctly accounts for inheritance โ€” a Dog genuinely is an Animal in any reasonable sense, and code that only checks with type() would incorrectly treat it as something else entirely. isinstance() also conveniently accepts a tuple of types, letting you check against several acceptable types in a single call.

๐Ÿ Pythonisinstance_multiple_types.py
def describe_number(value):
    if isinstance(value, (int, float)):
        print(f"{value} is a number.")
    else:
        print(f"{value} is not a number.")

describe_number(7)      # 7 is a number.
describe_number(7.5)    # 7.5 is a number.
describe_number("7")    # 7 is not a number.

Interview Questions on Python Data Types

Practice Questions โ€” Test Your Understanding

1. What data type does each of the following belong to: 42, 42.0, "42", [42], (42,), {42}, {"answer": 42}, None, True?

Easy

2. Write code that converts the string "3.14" into a float and then rounds it to one decimal place.

Easy

3. Given the list [1, 2, 2, 3, 3, 3], write a single line of code to get a collection containing only the unique values.

Medium

4. Explain why the following raises an error, and rewrite it correctly: my_tuple = (10)\nmy_tuple[0] = 20

Medium

5. Write a dictionary representing a product with keys for name, price, and in_stock, then update the price and print the final dictionary.

Medium

6. What will print(bool("False")) output, and why might this surprise someone new to Python?

Hard

7. Explain why (1, 2, [3, 4]) is a valid tuple to create, but cannot be used as a dictionary key.

Hard

8. A function needs to accept either an int or a float as an argument and reject anything else. Write a check using isinstance() that accomplishes this correctly, including rejecting a bool (since bool is technically a subclass of int).

Hard

Conclusion โ€” Data Types as a Vocabulary, Not Just Rules

It's tempting to treat data types as a dry list of syntax to memorise โ€” square brackets for this, curly braces for that โ€” but the more useful way to think about them is as a vocabulary for describing the actual shape of the problem you're solving. A list says 'order matters, and this collection can change.' A tuple says 'this grouping is fixed and complete.' A set says 'I only care about uniqueness and membership.' A dictionary says 'I need to look things up by name, not position.' Choosing the type that actually matches your intent, rather than whichever one happens to feel familiar, tends to produce code that's not just correct, but genuinely easier to read and reason about later.

The mutability distinction covered throughout this guide is worth holding onto especially closely, since it resurfaces constantly once you move on to functions, default arguments, and later, object-oriented programming. Get comfortable checking a value's type with type() or isinstance() whenever something behaves unexpectedly, and treat that instinct as a normal, everyday debugging habit rather than a sign that something's gone wrong with your understanding of the language.

Frequently Asked Questions (FAQ)