Python Data Types and Strings
Go deeper into Python's type system — from integer division and boolean short-circuits to powerful string slicing and formatting. Built for students who already know variables, if/else, and loops.
2 modules · 8 lessons · free to read
What you'll learn
- ✓Use Python's numeric types precisely — choosing between int, float, and complex based on what the problem needs
- ✓Predict how Python handles truthy/falsy values and short-circuit logic in real conditional expressions
- ✓Slice, search, format, and iterate over strings using Python's built-in sequence operations
- ✓Apply type-awareness skills: check types with type() and isinstance(), and convert between numeric types correctly
01Numbers, Booleans, and None
Dig into the numeric types you already use and discover the ones you haven't met yet. You will learn exactly when Python gives you an int versus a float, how boolean logic short-circuits in practice, and what None really means inside a type-aware program.
1.Integers and Floats
An integer (int) is a whole number with no decimal point — like 3, -7, or 1000. A float is a number with a decimal component — like 3.14, -0.5, or 2.0. Python treats these two types differently, and knowing which type you have determines exactly what you get from arithmetic.
The most important distinction is division. The / operator always returns a float, even when the result is a whole number:
pythonprint(7 / 2) # 3.5 print(6 / 2) # 3.0 ← float, not int print(type(6 / 2)) # <class 'float'>
When you want integer division — discarding the decimal remainder — use // (floor division). It returns the largest whole number that does not exceed the true result:
pythonprint(7 // 2) # 3 print(-7 // 2) # -4 ← floors toward negative infinity print(type(7 // 2)) # <class 'int'>
Modulo, Exponentiation, and Precision
The % operator returns the remainder after floor division. It pairs naturally with //:
pythonprint(7 % 2) # 1 (7 = 3 * 2 + 1) print(10 % 3) # 1 (10 = 3 * 3 + 1) print(9 % 3) # 0 (divisible evenly)
Exponentiation uses **. Both int and float exponents are valid:
pythonprint(2 ** 10) # 1024 print(9 ** 0.5) # 3.0 (square root)
Floats have a precision limit because computers store them in binary. This occasionally produces surprising results:
pythonprint(0.1 + 0.2) # 0.30000000000000004
For financial or exact-decimal calculations, use int arithmetic or the decimal module. For most everyday maths in Python, float precision is more than adequate. Use // and % together whenever you need to split a number into whole parts and leftovers — such as converting seconds into minutes and remaining seconds.
Constraints
- –Use `//` for the minutes and `%` for the remaining seconds.
- –Return a tuple — not a list or two separate values.
2.Booleans Deep Dive
A boolean is either True or False — but Python does not require a value to literally be True or False to behave like one. Every Python value has a built-in truthiness: it is either truthy (treated as True in a boolean context) or falsy (treated as False).
The falsy values are a short, fixed list:
Falseitself0(integer zero) and0.0(float zero)None""(empty string)[],{},()(empty collections)
Every other value is truthy. This means you can write if my_list: instead of if len(my_list) > 0: — they mean exactly the same thing:
pythonname = "" if name: print("Hello, " + name) else: print("No name given") # prints this
Short-Circuit Evaluation
Python evaluates and and or expressions lazily: it stops as soon as it knows the answer. This is called short-circuit evaluation.
a and b— ifais falsy, Python returnsaimmediately (never evaluatesb)a or b— ifais truthy, Python returnsaimmediately (never evaluatesb)
Critically, and and or do not return True or False — they return one of their operands:
pythonprint(0 or "default") # "default" — 0 is falsy, so or returns right side print("hello" or "x") # "hello" — "hello" is truthy, so or returns left side print(None and 42) # None — None is falsy, so and returns left side print(5 and 10) # 10 — 5 is truthy, so and returns right side
The or pattern is commonly used to supply default values: result = value or "unknown". The and pattern is used to guard against falsy inputs before processing them. Use short-circuit evaluation whenever you want concise, readable conditional logic without nested if blocks.
Constraints
- –Use the `and` operator for short-circuit evaluation — do not use an `if` statement.
- –Return the empty string `""` (falsy) when `s` is empty, and the first character when it is not.
3.None and Type Awareness
None is Python's way of representing the absence of a value. It is its own type (NoneType) and its own value — not 0, not False, not an empty string. When a function does not explicitly return anything, Python returns None automatically.
pythondef do_nothing(): pass result = do_nothing() print(result) # None print(type(result)) # <class 'NoneType'>
None is falsy, but it is not equivalent to other falsy values. Use is None — not == None — to check for it explicitly:
pythonvalue = None if value is None: print("Nothing here") # correct check
type() and isinstance()
type(x) returns the exact type of x as a type object. You can compare it to built-in types like int, float, str, or bool:
pythonprint(type(42)) # <class 'int'> print(type(3.14)) # <class 'float'> print(type("hello")) # <class 'str'> print(type(True)) # <class 'bool'> print(type(42) == int) # True print(type(3.14) == int) # False
isinstance(x, T) is usually preferred because it handles inheritance correctly and accepts a tuple of types:
pythonprint(isinstance(42, int)) # True print(isinstance(True, int)) # True ← bool IS a subclass of int print(isinstance(42, (int, float))) # True ← checks both
Type coercion in Python is mostly explicit — you call int(), float(), or str() yourself. Python does perform one implicit conversion: mixing int and float in arithmetic always produces a float. Use type() to inspect, isinstance() to branch, and explicit casts to convert. Knowing your types prevents entire classes of bugs before they happen.
Constraints
- –Use `isinstance()` — do not use `type(x) ==` comparisons.
- –Check for `bool` before `int` because `bool` is a subclass of `int` in Python.
4.Complex Numbers and the Python Type System
A complex number has a real part and an imaginary part. In Python, you write the imaginary component with a j suffix: 3 + 4j. The built-in complex type stores both parts and supports arithmetic with other numeric types.
pythonz = 3 + 4j print(type(z)) # <class 'complex'> print(z.real) # 3.0 print(z.imag) # 4.0 print(abs(z)) # 5.0 (magnitude: sqrt(3² + 4²))
You can also create complex numbers with the complex() constructor:
pythonz = complex(3, 4) # same as 3 + 4j print(z) # (3+4j)
The Numeric Conversion Tower
Python's three numeric types form a widening hierarchy: int → float → complex. When you mix types in an expression, Python automatically promotes to the wider type:
pythonprint(type(1 + 2.0)) # <class 'float'> int + float → float print(type(1.0 + (2+0j))) # <class 'complex'> float + complex → complex print(type(1 + (2+0j))) # <class 'complex'> int + complex → complex
Explicit conversion follows the same direction — you can always go wider, but going narrower can lose information:
pythonprint(float(5)) # 5.0 print(int(3.9)) # 3 (truncates — does NOT round) print(complex(2)) # (2+0j) # complex → float or int raises TypeError — no automatic narrowing
In practice, use int for counting and indexing, float for measurement and maths, and complex for signal processing or physics. Knowing the tower explains why True + 1.5 equals 2.5 — Python converts True (an int subclass with value 1) to float before adding.
Constraints
- –Add `a` and `b` to get the result, then use `isinstance()` to classify it.
- –Check for `complex` before `float` before `int` — in widest-first order.
02Working with Strings
Strings are sequences, and Python treats them like one. This module teaches you to slice, search, transform, format, and iterate over strings using Python's rich built-in toolkit — turning text manipulation from guesswork into precision.
1.Indexing and Slicing
A string in Python is a sequence — an ordered collection of characters, where each character has a numbered position called an index. Indexing starts at 0, so s[0] is the first character, s[1] is the second, and so on. Python also supports negative indices: s[-1] is the last character, s[-2] is the second to last.
pythons = "python" print(s[0]) # 'p' print(s[5]) # 'n' print(s[-1]) # 'n' (same as s[5]) print(s[-2]) # 'o'
If you try to access an index that does not exist, Python raises an IndexError. Always stay within the range 0 to len(s) - 1 (or -1 to -len(s) for negative).
Slicing
A slice extracts a substring using the syntax s[start:stop:step]. start is inclusive, stop is exclusive:
pythons = "python" print(s[1:4]) # 'yth' (indices 1, 2, 3) print(s[:3]) # 'pyt' (from beginning to index 3) print(s[3:]) # 'hon' (from index 3 to end) print(s[::2]) # 'pto' (every other character) print(s[::-1]) # 'nohtyp' (reversed — step of -1)
Omitting start defaults to 0; omitting stop defaults to the end of the string. The step controls how far to jump between characters. A step of -1 walks the string backwards, which is the standard Pythonic way to reverse a string. Use slicing any time you need to extract a prefix, suffix, or every nth character — it is always more readable than a loop.
Constraints
- –Use slice notation `s[::-1]` to reverse the string — do not use a loop.
- –Return the concatenation of the original string and its reverse.
2.String Methods
Python strings come with dozens of built-in methods — functions you call on the string object itself using dot notation. Because strings are immutable (they never change in place), every method returns a new string rather than modifying the original.
The most common transformation methods:
pythons = " Hello, World! " print(s.strip()) # 'Hello, World!' removes leading/trailing whitespace print(s.strip().lower()) # 'hello, world!' chain methods print(s.upper()) # ' HELLO, WORLD! ' print(s.replace("World", "Python")) # ' Hello, Python! '
Methods can be chained — calling one after another in a single expression. The output of each call becomes the input of the next, reading left to right.
Splitting and Joining
.split(sep) breaks a string into a list of substrings wherever sep appears. With no argument it splits on any whitespace:
pythonprint("one,two,three".split(",")) # ['one', 'two', 'three'] print("hello world".split()) # ['hello', 'world']
.join(iterable) is the reverse — it glues a list of strings together using the calling string as the separator:
pythonwords = ["python", "is", "great"] print(" ".join(words)) # 'python is great' print("-".join(words)) # 'python-is-great' print("".join(words)) # 'pythonisgreat'
The pattern sep.join(s.split(old_sep)) is the idiomatic way to swap one separator for another. Use .strip() first when processing user input, .lower() when you need case-insensitive comparison, and .split() + .join() whenever you need to reformat delimited text.
Constraints
- –Use `.strip()`, `.lower()`, and `.split()` + `.join()` — do not use a loop or regular expressions.
- –Return a single string with words separated by exactly one space.
3.Finding and Formatting
Python provides four essential methods for searching inside a string without extracting any characters. .find(sub) returns the index of the first occurrence of sub, or -1 if not found. .count(sub) returns how many non-overlapping times sub appears. .startswith(prefix) and .endswith(suffix) return True or False:
pythons = "hello, world" print(s.find("world")) # 7 print(s.find("xyz")) # -1 print(s.count("l")) # 3 print(s.startswith("hello")) # True print(s.endswith("!")) # False
These methods are case-sensitive. Use .lower() first if you need a case-insensitive search.
f-string Format Specifications
f-strings let you embed expressions directly in strings — and a format spec after : inside {} controls how the value is displayed.
For numbers, :.2f formats a float to exactly 2 decimal places:
pythonprice = 9.5 print(f"Price: {price:.2f}") # 'Price: 9.50'
For alignment, :>N right-aligns in a field of width N, :<N left-aligns, and :^N centres:
pythonname = "Alice" print(f"{name:>10}") # ' Alice' (right-aligned in 10 chars) print(f"{name:<10}|") # 'Alice |' (left-aligned) print(f"{name:^10}") # ' Alice ' (centred)
Format specs are especially useful when you need to produce aligned output — like a receipt, a table column, or a report. Use :.2f any time you display currency or measurements, and :>N / :<N when columns must line up.
Constraints
- –Use an f-string with format specs — do not use string multiplication or manual padding.
- –Name field: left-aligned, width 15 (`:<15`). Price field: right-aligned, width 8, 2 decimal places (`:>8.2f`).
4.Strings as Sequences
A Python string is a sequence in the full sense of the word — you can iterate over it with a for loop exactly like you would a list. Each iteration gives you one character:
pythonfor char in "hello": print(char) # h # e # l # l # o
The in operator tests membership — whether a character or substring exists anywhere in the string:
pythonprint("e" in "hello") # True print("xyz" in "hello") # False print("ell" in "hello") # True (substring check)
Similarly, len(s) returns the number of characters in the string.
Immutability and Building New Strings
Strings are immutable — you cannot change a character at an index in place:
pythons = "hello" s[0] = "H" # TypeError: 'str' object does not support item assignment
Instead, build a new string. The most Pythonic approach is to construct a list of the characters you want, then join them:
pythonresult = [] for char in "hello": if char != "l": result.append(char) print("".join(result)) # 'heo'
For simple filters, a list comprehension is even cleaner:
pythonprint("".join(c for c in "hello" if c != "l")) # 'heo'
The pattern "".join(...) is the standard way to build a new string from parts — always prefer it over concatenating inside a loop with +=, which is slower. Use for c in s when you need to inspect or transform individual characters, in for membership checks, and "".join(list) to assemble the result.
Constraints
- –Use `"".join(...)` to build the result — do not use string concatenation inside a loop.
- –Remove both uppercase and lowercase vowels: a, e, i, o, u, A, E, I, O, U.
Frequently Asked Questions
- What is the value and type of the result? ```python result = 7 // 2 ```
- 3 (int). The `//` operator performs floor division — it divides and rounds down to the nearest integer. 7 ÷ 2 = 3.5, so floor division gives 3 as an `int`. The regular `/` operator would return 3.5 as a float.
- Which of the following values is **truthy** in Python?
- "False". The string `"False"` is truthy because it is a non-empty string. Python only treats empty strings as falsy. The values `0`, `""`, and `[]` are all falsy — zero, empty string, and empty list respectively.
- What does this expression evaluate to? ```python result = "" or "default" ```
- "default". The `or` operator uses short-circuit evaluation and returns one of its operands, not a boolean. Since `""` is falsy, Python evaluates the right side and returns `"default"`. If the left side had been truthy, it would have returned that instead.
- Which check correctly identifies `None` in Python?
- if value is None:. The correct idiom is `is None` — it checks identity (that the value is the exact `None` object), not equality. `== None` works but is considered poor style. `not value` is too broad, catching `0`, `""`, and `[]` as well as `None`.
- What is the result of this expression? ```python type(1 + 2.0) == float ```
- True — mixing int and float always widens to float. Python's numeric widening tower promotes `int` to `float` whenever the two types are mixed in arithmetic. `1 + 2.0` produces `3.0` — a `float`. This is called implicit type promotion, and it always goes in the direction of the wider type.
- How do I write a Python function `split_seconds(total_seconds)` that uses `//` and `%` to split a total number of seconds into a tuple of `(minutes, seconds)`.?
- An **integer** (`int`) is a whole number with no decimal point — like `3`, `-7`, or `1000`. A **float** is a number with a decimal component — like `3.14`, `-0.5`, or `2.0`. Python treats these two types differently, and knowing which type you have determines exactly what you get from arithmetic.
- How do I write a Python function `safe_first_char(s)` that returns the first character of the string `s` if `s` is non-empty, or returns the empty string `""` if `s` is empty — using short-circuit evaluation with `and`.?
- A **boolean** is either `True` or `False` — but Python does not require a value to literally be `True` or `False` to behave like one. Every Python value has a built-in **truthiness**: it is either *truthy* (treated as `True` in a boolean context) or *falsy* (treated as `False`).
- How do I write a Python function `describe_number(x)` that returns the string `"bool"`, `"int"`, `"float"`, or `"other"` depending on the type of `x`, using `isinstance()` to check each type in the correct order.?
- `None` is Python's way of representing **the absence of a value**. It is its own type (`NoneType`) and its own value — not `0`, not `False`, not an empty string. When a function does not explicitly `return` anything, Python returns `None` automatically.
- How do I write a Python function `numeric_type_name(a, b)` that adds `a` and `b` together and returns the string `"int"`, `"float"`, or `"complex"` depending on the type of the result — demonstrating Python's widening conversion tower.?
- A **complex number** has a real part and an imaginary part. In Python, you write the imaginary component with a `j` suffix: `3 + 4j`. The built-in `complex` type stores both parts and supports arithmetic with other numeric types.
- What does `s[::-1]` do when `s = "python"`?
- Reverses the string: 'nohtyp'. A step of `-1` tells the slice to walk the string backwards. With no start or stop specified, it goes from the last character to the first, producing the reversed string `'nohtyp'`.
- What is the output of this code? ```python print(" ".join("hello world".split())) ```
- hello world. `.split()` with no argument splits on whitespace and returns `['hello', 'world']`. Then `' '.join(...)` reassembles that list with a single space between each element, producing `'hello world'`. This is the standard way to normalise whitespace.
- What does `.find()` return when the substring is **not** found?
- -1. `.find()` always returns an integer — the index of the first occurrence, or `-1` if the substring is absent. This is why you should check `if s.find(sub) != -1` rather than `if s.find(sub)` (since index 0 is also falsy).
- Why does this code raise a `TypeError`? ```python s = "hello" s[0] = "H" ```
- Because strings in Python are immutable — characters cannot be reassigned. Python strings are immutable — once created, their characters cannot be changed in place. To 'modify' a string you must create a new one, for example with `'H' + s[1:]` or `s.replace('h', 'H', 1)`.
- What is the result of this expression? ```python "ell" in "hello" ```
- True — in checks for substrings too. The `in` operator on strings tests for **substring** membership, not just single characters. `"ell" in "hello"` returns `True` because `"ell"` appears starting at index 1. This is different from lists, where `in` only checks for exact elements.
- How do I write a Python function `mirror(s)` that returns the string `s` concatenated with its own reverse — creating a palindrome-like reflection.?
- A string in Python is a **sequence** — an ordered collection of characters, where each character has a numbered position called an **index**. Indexing starts at 0, so `s[0]` is the first character, `s[1]` is the second, and so on. Python also supports **negative indices**: `s[-1]` is the last character, `s[-2]` is the second to last.
- How do I write a Python function `normalize(s)` that strips leading/trailing whitespace, lowercases the string, and collapses all internal whitespace runs into single spaces — using chained string methods.?
- Python strings come with dozens of built-in **methods** — functions you call on the string object itself using dot notation. Because strings are **immutable** (they never change in place), every method returns a *new* string rather than modifying the original.
- How do I write a Python function `format_price_tag(name, price)` that returns a formatted string with `name` left-aligned in a field of width 15 and `price` right-aligned in a field of width 8 with 2 decimal places.?
- Python provides four essential methods for searching inside a string without extracting any characters. `.find(sub)` returns the index of the first occurrence of `sub`, or `-1` if not found. `.count(sub)` returns how many non-overlapping times `sub` appears. `.startswith(prefix)` and `.endswith(suffix)` return `True` or `False`:
- How do I write a Python function `remove_vowels(s)` that returns a new string with all vowels (both upper and lower case) removed, using iteration and `"".join()`.?
- A Python string is a **sequence** in the full sense of the word — you can iterate over it with a `for` loop exactly like you would a list. Each iteration gives you one character:
Ready to write code?
Theory is just the start. Write real code, run tests, build the habit.
Open the playground →