Python · lesson 4 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Python Data Types
Master Python's built-in data types — int, float, str, bool, None — and how to inspect and convert between them with real type conversion.
By SkillByExample · Updated
The Core Built-in Types
Python has a small set of primitive types that everything else is built from. Getting them right early prevents bugs that are surprisingly hard to track down later — especially around float precision, boolean coercion, and the None vs falsy value distinction. Output below is from Python 3.13.
int
Python integers have arbitrary precision — there is no overflow, no matter how large the number gets. This is different from most other languages, where integers wrap around at a fixed bit size.
big = 10 ** 100 # a googol: no overflow
print(len(str(big))) # 101 digits
print(2 ** 64 + 1) # past the 64-bit limit other languages hit
print(0xFF, 0o17, 0b1010) # hex, octal and binary literals
print(1_000_000) # underscores are only for readability
print(type(big))
$ python ints.py
101
18446744073709551617
255 15 10
1000000
<class 'int'>
2 ** 64 + 1 would wrap around in a language with fixed 64-bit integers; Python just allocates more digits. The other bases are only different ways to write a literal: once parsed, 0xFF is the ordinary int 255, and the underscores in 1_000_000 vanish entirely.
Integer division behavior is one of Python 3’s key changes from Python 2 — / now always returns a float:
# types.py
print(7 // 2) # floor division
print(7 / 2) # true division
print(7 % 2) # modulo
$ python types.py
3
3.5
1
// rounds toward negative infinity (floor division); / always returns a float (true division); % gives the remainder. In Python 2, / performed integer division when both operands were integers — a major source of bugs during the 2→3 transition.
float
Floats are 64-bit IEEE 754 doubles — the same format used by virtually every other language. The precision pitfall they carry is not a Python bug; it’s a consequence of representing decimal fractions in binary.
pi = 3.14159
sci = 1.5e10 # scientific notation
neg_exp = 2.5e-3 # negative exponent
# The classic precision pitfall — affects every language using IEEE 754
print(0.1 + 0.2 == 0.3) # False!
print(0.1 + 0.2) # the actual stored value
# Fix with round() when displaying to users
print(round(0.1 + 0.2, 1) == 0.3) # True
# Fix with decimal for financial math — never use float for money
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))
$ python types.py
False
0.30000000000000004
True
0.3
Float arithmetic is exact for the IEEE 754 representation, but 0.1 has no exact binary representation — so 0.1 + 0.2 lands at 0.30000000000000004, not 0.3. round() fixes display; Decimal fixes computation. Never use float for money.
Special float values handle edge cases like division limits and invalid math:
import math
# These print without errors, but produce special sentinel values
print(math.inf) # float infinity
print(-math.inf) # negative infinity
print(math.inf - math.inf) # nan: "not a number"
print(math.nan == math.nan) # nan is not equal to anything, even itself
print(math.isnan(math.nan))
try:
print(0.0 / 0.0)
except ZeroDivisionError as e:
print(f"ZeroDivisionError: {e}")
$ python special_floats.py
inf
-inf
nan
False
True
ZeroDivisionError: float division by zero
Unlike C or JavaScript, Python does not return nan or inf for division by zero, even with floats: 0.0 / 0.0 raises ZeroDivisionError. nan comes from undefined operations like inf - inf, and because it compares unequal to itself, test for it with math.isnan(). math.inf is a handy starting value when searching for a minimum.
str
Strings are immutable sequences of Unicode code points. Every string method returns a new string — the original is never modified. Python 3 made all strings Unicode by default, which means you can use any language or emoji without special handling.
# strings.py
name = "Alice"
multiline = """Line one
Line two"""
raw = r"C:\Users\alice\file.txt"
age = 30
greeting = f"Hello, {name}! You are {age} years old."
result = f"2 + 2 = {2 + 2}"
formatted = f"Pi is approximately {3.14159:.2f}"
print(greeting)
print(result)
print(formatted)
print("hello".upper())
print(" spaces ".strip())
print("a,b,c".split(","))
print("-".join(["a", "b"]))
print("hello world".replace("world", "Python"))
$ python strings.py
Hello, Alice! You are 30 years old.
2 + 2 = 4
Pi is approximately 3.14
HELLO
spaces
['a', 'b', 'c']
a-b
hello Python
Strings are sequences, so the same indexing and slicing syntax used on lists works here too:
# indexing.py
s = "Python"
print(s[0])
print(s[-1])
print(s[1:4])
print(s[::-1])
$ python indexing.py
P
n
yth
nohtyP
bool
bool is a subclass of int, which means True and False are literally 1 and 0. This has practical consequences — you can sum a list of booleans to count how many are True.
# bool_arithmetic.py
print(True + True)
print(bool(0))
print(bool(1))
print(bool(""))
print(bool("hello"))
print(bool([]))
print(bool([0]))
$ python bool_arithmetic.py
2
False
True
False
True
False
True
bool is a subclass of int, so True + True equals 2. Remember: bool([0]) is True because the list is non-empty — even though 0 inside it is falsy.
Memorizing Python’s falsy values prevents bugs in conditional checks. The bool() constructor reveals truthiness without needing an if statement:
# falsy_values.py
print(bool(False)) # False
print(bool(None)) # False
print(bool(0)) # False
print(bool(0.0)) # False
print(bool("")) # False
print(bool([])) # False
print(bool({})) # False
$ python falsy_values.py
False
False
False
False
False
False
False
These are the common falsy values; the rest of the family is any other zero or empty container — 0j, Decimal(0), (), set(), b"", range(0) — and everything else is truthy. Remembering which values are falsy is the quickest way to avoid subtle bugs in conditional checks.
None
None is Python’s null value — it represents “no value” or “not yet set.” It’s a singleton: there is exactly one None object in any Python process. Use it to signal optional values, uninitialized state, or missing results. Always check for None with is, not ==, since is checks identity (same object in memory) — correct for a singleton.
# none_demo.py
result = None
if result is None:
print("No result yet")
if result is not None:
print("We have a result!")
else:
print("Result is still None")
print(type(None))
$ python none_demo.py
No result yet
Result is still None
<class 'NoneType'>
type() and isinstance()
Inspecting types at runtime is useful for debugging and for writing functions that handle multiple input types. isinstance() is almost always preferable to type() because it respects inheritance.
# types.py
print(type(42))
print(type("hello"))
print(type(3.14))
print(type(True))
print(type(None))
# isinstance() is aware of inheritance — bool IS-A int
print(isinstance(42, int))
print(isinstance(True, int))
print(isinstance(42, (int, float)))
$ python types.py
<class 'int'>
<class 'str'>
<class 'float'>
<class 'bool'>
<class 'NoneType'>
True
True
True
Type Conversion
Python doesn’t implicitly convert between types (except in a few arithmetic cases). Explicit conversion makes your intent clear and avoids silent data loss.
# conversions.py
print(int("42")) # parses a decimal string
print(int(3.9)) # truncates toward zero, does NOT round
print(float("3.14")) # decimal string to float
print(str(100)) # number to string
print(bool(0)) # falsy value to False
print(list("abc")) # string splits into individual characters
# int() accepts a base argument for non-decimal strings
print(int("FF", 16)) # hexadecimal
print(int("1010", 2)) # binary
$ python conversions.py
42
3
3.14
100
False
['a', 'b', 'c']
255
10
Type Hints
Type hints don’t affect runtime behavior — Python ignores them at execution time. Their value is for tooling: editors use them for autocomplete, mypy uses them to catch type errors before you run the code, and they serve as inline documentation for anyone reading the function signature.
from typing import Optional
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}"
def find_user(user_id: int) -> Optional[str]:
if user_id == 1:
return "Alice"
return None # explicit None is self-documenting
print(add(3, 5))
print(greet("Bob"))
print(find_user(1))
print(find_user(99))
$ python type_hints.py
8
Hello, Bob
Alice
None
In Python 3.10+, you can use the union syntax directly without importing from typing:
def find_user(user_id: int) -> str | None:
if user_id == 1:
return "Alice"
return None
print(find_user(1))
$ python union_syntax.py
Alice
Common Pitfalls
Mutable default arguments — one of Python’s most infamous gotchas. Default argument values are evaluated once when the function is defined, not each time it’s called. A mutable default (like a list or dict) is shared across every call.
# mutable_default.py
def append_to_wrong(item, lst=[]):
lst.append(item)
return lst
result1 = append_to_wrong(1)
result2 = append_to_wrong(2)
print(result1)
print(result2)
$ python mutable_default.py
[1, 2]
[1, 2]
Even result1 prints [1, 2], although it was returned before 2 was appended. Both calls returned the same list object, the one default created when def ran, so result1 and result2 are two names for one list. The fix is to use None as the sentinel:
# correct_default.py
def append_to(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
result3 = append_to(1)
result4 = append_to(2)
print(result3)
print(result4)
$ python correct_default.py
[1]
[2]
Integer identity caching — CPython caches small integers (-5 to 256) as an optimization, which means is comparisons appear to work for small ints but break for larger ones. Never use is to compare integers.
# int_cache.py
a = int("256")
b = int("256")
print(a is b) # True: within the cached range
a = int("257")
b = int("257")
print(a is b) # False: outside the cache, two different objects
print(a == b) # True: value equality still works
c, d = 257, 257
print(c is d) # True, but only because the compiler reused one constant
$ python int_cache.py
True
False
True
True
The values are built with int("...") so each is created at runtime. Write 257 twice as a literal in the same script and the compiler may store a single constant, so is says True anyway, as the last line shows. That inconsistency is the point: whether is happens to work depends on caching and compiler details.
Always use == for value comparison, never is. The identity check is an implementation detail of CPython, not a language guarantee — other Python implementations (PyPy, Jython) may not cache at all.
Practice
1. Write a program that adds 0.1 + 0.2 and prints the result. Then use the decimal module to show the exact decimal addition. Predict the difference before you run it.
Solution
from decimal import Decimal
print(0.1 + 0.2)
print(Decimal("0.1") + Decimal("0.2"))
$ python float_precision.py
0.30000000000000004
0.3
The float gives 0.30000000000000004 — the nearest binary representation is slightly too large. Decimal("0.1") stores exactly one-tenth, so the sum is exactly 0.3.
2. Predict what bool() returns for 0, -1, " ", [] and [0], then run it.
Solution
print(bool(0))
print(bool(-1))
print(bool(" "))
print(bool([]))
print(bool([0]))
$ python bool_coercion.py
False
True
True
False
True
0 is falsy but any non-zero number — even -1 — is truthy. A non-empty string (even just a space) is truthy. An empty list is falsy, but [0] is truthy because the list has one element, even though that element (0) is itself falsy.
3. Write a function that returns True when given a bool but False when given a regular int. Use isinstance() to make it work.
Solution
def is_bool_not_int(value):
return isinstance(value, bool)
print(is_bool_not_int(True)) # True — a bool is a bool
print(is_bool_not_int(42)) # False — an int is not a bool
print(is_bool_not_int(1)) # False — even though 1 == True
$ python isinstance_bool.py
True
False
False
The tricky part: since bool subclasses int, isinstance(True, int) is also True. The check must distinguish a bool from a plain int — isinstance(value, bool) alone works since bool is more specific than int.