Data Types & Structures Loops & Control Flows
Programming foundation - Storing and working with different kinds of data
Variables in Python are symbolic names pointing to objects or values in memory. Unlike many other languages, Python has no command for declaring a variable — a variable is created the moment you first assign a value to it.
Python has several built-in data types. Understanding when to use each type is fundamental to writing effective Python code.
Numeric Data Types
Python has three numeric types:
- int — whole numbers with no size limit:
42,-7,1_000_000 - float — 64-bit decimal numbers:
3.14,2.5e3(= 2500.0) - complex — real + imaginary:
3 + 4j
Key built-in functions:
abs(x)— absolute valueround(x, n)— round to n decimal placespow(x, y)— same asx ** ydivmod(x, y)— returns(quotient, remainder)as a tupleint(x),float(x),str(x)— type conversion
Note: int(3.9) gives 3 — it truncates, it does not round.
# ── int ──────────────────────────────────────────────────────x = 42big = 1_000_000 # underscores improve readabilitybin_ = 0b1010 # binary literal → 10hex_ = 0xFF # hex literal → 255print(x, big, bin_, hex_) # ── float ─────────────────────────────────────────────────────pi = 3.14159sci = 2.5e3 # scientific notation → 2500.0print(pi, sci) # ── complex ───────────────────────────────────────────────────c = 3 + 4jprint(c.real, c.imag) # 3.0 4.0print(abs(c)) # 5.0 (magnitude √(3²+4²)) # ── type conversion ───────────────────────────────────────────print(int(3.9)) # 3 ← truncates, does NOT roundprint(float(7)) # 7.0print(str(42)) # '42'print(bool(0)) # Falseprint(bool(-5)) # True # ── useful built-ins ──────────────────────────────────────────print(abs(-15)) # 15print(round(3.14159, 2)) # 3.14print(pow(2, 10)) # 1024print(divmod(17, 5)) # (3, 2) quotient and remainderprint(max(3, 7, 1, 9)) # 9print(min(3, 7, 1, 9)) # 1
String Data Type
Strings are immutable sequences of characters. All string methods return a new string — they never modify the original.
Use single '...' or double "..." quotes. Triple quotes """...""" span multiple lines.
Case & whitespace: upper(), lower(), title(), strip(), lstrip(), rstrip()
Search: find(s) → index or -1, count(s), startswith(s), endswith(s)
Test: isdigit(), isalpha(), isalnum(), isspace()
Modify: replace(old, new), split(sep), join(iterable)
Format: f-strings f"..." — embed any expression inside {}
s = " Hello, Python World! " # ── case & whitespace ─────────────────────────────────────────print(s.strip()) # "Hello, Python World!"print(s.lower())print(s.upper())print("hElLo".swapcase()) # "HeLlO" # ── search ────────────────────────────────────────────────────t = "Hello, Python World!"print(t.find("Python")) # 7 (index; -1 if not found)print(t.count("l")) # 3print(t.startswith("Hello")) # Trueprint(t.endswith("!")) # True # ── test ──────────────────────────────────────────────────────print("123".isdigit()) # Trueprint("abc".isalpha()) # Trueprint("abc123".isalnum()) # True # ── modify ────────────────────────────────────────────────────print(t.replace("Python", "Data Science"))print("a,b,c,d".split(",")) # ['a', 'b', 'c', 'd']print("-".join(["2024", "01", "15"])) # '2024-01-15' # ── f-strings ─────────────────────────────────────────────────name = "Alice"score = 98.567print(f"{name} scored {score:.2f}%") # Alice scored 98.57%print(f"{'Python':^20}") # centred in 20 charsprint(f"{42:08b}") # 00101010 (binary)
Indexing & Slicing
Python uses zero-based indexing. Negative indices count from the right (-1 is the last element).
Slice syntax: seq[start : stop : step] — stop is always exclusive. Use -1 as step to reverse. Works on strings, lists, and tuples.
text = "Python"nums = [10, 20, 30, 40, 50] # ── indexing ──────────────────────────────────────────────────print(text[0], text[-1]) # P nprint(nums[2], nums[-2]) # 30 40 # ── slicing strings ───────────────────────────────────────────print(text[0:3]) # Pytprint(text[2:]) # thonprint(text[:3]) # Pytprint(text[::2]) # Pto every 2nd charprint(text[::-1]) # nohtyP reversed # ── slicing lists ─────────────────────────────────────────────print(nums[1:4]) # [20, 30, 40]print(nums[::2]) # [10, 30, 50]print(nums[::-1]) # [50, 40, 30, 20, 10] # ── slicing returns a NEW object ──────────────────────────────subset = nums[1:3]print(subset) # [20, 30]print(nums) # [10, 20, 30, 40, 50] unchanged
Lists
A list is an ordered, mutable sequence. You can add, remove, and change elements after creation.
Add: append(x), insert(i, x), extend(iterable)
Remove: remove(x) by value, pop(i) by index (returns the item), clear()
Search: index(x) → first position, count(x) → occurrences
Order: sort() modifies in place; sorted(lst) returns a new sorted list
Copy: copy() — without it, two variables share the same list object.
fruits = ["apple", "banana", "cherry"] # ── add ───────────────────────────────────────────────────────fruits.append("date") # add to endfruits.insert(1, "avocado") # insert at index 1fruits.extend(["elderberry", "fig"]) # add multipleprint(fruits) # ── remove ────────────────────────────────────────────────────fruits.remove("avocado") # remove by value (first match)popped = fruits.pop() # remove & return last itemprint(f"Popped: {popped}") # ── search ────────────────────────────────────────────────────nums = [3, 1, 4, 1, 5, 9, 2, 6]print(nums.index(4)) # 2print(nums.count(1)) # 2 # ── sort ──────────────────────────────────────────────────────nums.sort() # in placeprint(nums)words = ["banana", "apple", "cherry"]print(sorted(words)) # new list — original unchangedprint(words) # ── mutability: two names, same list ─────────────────────────a = [1, 2, 3]b = ab.append(4)print(a) # [1, 2, 3, 4] ← also changed!c = a.copy() # independent copyc.append(99)print(a) # [1, 2, 3, 4] ← unchanged
Tuples
A tuple is an ordered, immutable sequence. Once created, you cannot add, remove, or change elements. A single-element tuple needs a trailing comma: (42,).
Use tuples for: coordinates, RGB values, function returns, and dictionary keys (lists cannot be keys).
Tuple unpacking assigns multiple variables in one line. Use * to capture remaining elements.
# ── creating tuples ───────────────────────────────────────────point = (3, 7)rgb = (255, 128, 0)single = (42,) # trailing comma required! # ── access ────────────────────────────────────────────────────print(point[0]) # 3print(rgb[-1]) # 0print(rgb[0:2]) # (255, 128) # ── unpacking ─────────────────────────────────────────────────x, y = pointprint(f"x={x}, y={y}") r, g, b = rgbprint(f"R={r} G={g} B={b}") first, *rest = (1, 2, 3, 4, 5)print(first, rest) # 1 [2, 3, 4, 5] # ── immutable ─────────────────────────────────────────────────try: point[0] = 10except TypeError as e: print(f"TypeError: {e}") # ── tuple as dict key ─────────────────────────────────────────loc = {(28.6, 77.2): "Delhi", (19.1, 72.9): "Mumbai"}print(loc[(28.6, 77.2)]) # Delhi # ── count & index ─────────────────────────────────────────────t = (1, 2, 2, 3, 2)print(t.count(2), t.index(3)) # 3 3
Dictionaries
A dictionary stores key-value pairs. Keys must be unique and immutable. Dicts are mutable and, since Python 3.7, preserve insertion order.
Use d.get(key, default) instead of d[key] when the key might be absent — it returns None (or a default) instead of raising a KeyError.
Views: keys(), values(), items() — live views, not copies.
person = {"name": "Alice", "age": 28, "city": "Jaipur"} # ── access ────────────────────────────────────────────────────print(person["name"]) # Aliceprint(person.get("salary")) # None (no KeyError)print(person.get("salary", 0)) # 0 (default) # ── add / update ──────────────────────────────────────────────person["email"] = "alice@example.com"person["age"] = 29person.update({"city": "Delhi", "role": "analyst"})print(person) # ── remove ────────────────────────────────────────────────────removed = person.pop("email")print(f"Removed: {removed}") # ── iterate ───────────────────────────────────────────────────for key, value in person.items(): print(f" {key}: {value}") # ── membership check ──────────────────────────────────────────print("name" in person) # Trueprint("salary" in person) # False # ── setdefault ────────────────────────────────────────────────person.setdefault("country", "India")print(person["country"]) # India
for Loops & range()
The for loop iterates over any iterable — lists, tuples, strings, dicts, and range objects.
range(): range(n) → 0 to n−1 | range(start, stop) → stop is exclusive | range(start, stop, step)
Extras: enumerate(iterable) yields (index, value) pairs; zip(a, b) pairs two iterables element by element.
# ── range variants ────────────────────────────────────────────for i in range(5): print(i, end=" ") # 0 1 2 3 4print()for i in range(1, 6): print(i, end=" ") # 1 2 3 4 5print()for i in range(0, 20, 5): print(i, end=" ") # 0 5 10 15print()for i in range(10, 0, -2): print(i, end=" ") # 10 8 6 4 2print() # ── iterating a list and string ───────────────────────────────fruits = ["apple", "banana", "cherry"]for fruit in fruits: print(fruit.upper()) for char in "Python": print(char, end="-") # P-y-t-h-o-n-print() # ── enumerate: index + value ──────────────────────────────────for i, fruit in enumerate(fruits, start=1): print(f"{i}. {fruit}") # ── zip: pair two lists ───────────────────────────────────────names = ["Alice", "Bob", "Charlie"]scores = [92, 85, 78]for name, score in zip(names, scores): print(f"{name}: {score}") # ── iterating a dictionary ────────────────────────────────────grades = {"Maths": "A", "Science": "B", "English": "A"}for subject, grade in grades.items(): print(f"{subject} → {grade}")
while Loops
A while loop runs as long as its condition is True. Use it when you don't know the number of iterations in advance.
while-else: the else block runs when the condition becomes False naturally — it is skipped if a break exits the loop.
# ── basic while ───────────────────────────────────────────────count = 1while count <= 5: print(f"Count: {count}") count += 1print("Done!") # ── countdown ─────────────────────────────────────────────────n = 10while n > 0: print(n, end=" ") n -= 3print(f"\nFinal n: {n}") # ── while-else ────────────────────────────────────────────────attempts = 0while attempts < 3: attempts += 1 print(f"Attempt {attempts}")else: print("All attempts used") # runs when loop ends naturally # ── search (break skips the else) ─────────────────────────────items = [4, 7, 2, 9, 1, 5]target = 9i = 0while i < len(items): if items[i] == target: print(f"Found {target} at index {i}") break i += 1else: print(f"{target} not found") # skipped because break fired
break, continue & pass
Three statements give fine-grained control inside any loop:
break— exit the loop immediately; skips theelseclausecontinue— skip the rest of the current iteration and move to the nextpass— do nothing; placeholder for an empty block while drafting code
# ── break ─────────────────────────────────────────────────────print("-- break --")for i in range(1, 10): if i == 5: print("Reached 5, stopping!") break print(i) # prints 1 2 3 4 # ── continue ──────────────────────────────────────────────────print("-- continue --")for i in range(1, 8): if i % 2 == 0: continue # skip even numbers print(i) # prints 1 3 5 7 # ── pass ──────────────────────────────────────────────────────print("-- pass --")for i in range(5): if i == 2: pass # placeholder — handle later else: print(i) # prints 0 1 3 4 # ── break + else pattern ──────────────────────────────────────numbers = [3, 7, 12, 5, 8]for num in numbers: if num > 10: print(f"First > 10: {num}") breakelse: print("None greater than 10") # runs only if no break
List Comprehension
List comprehension creates a list from an iterable in a single, readable expression — faster and more concise than a for-loop + append.
Syntax: [expression for item in iterable if condition] — the if part is optional.
# ── basic ─────────────────────────────────────────────────────squares = [x**2 for x in range(1, 6)]print(squares) # [1, 4, 9, 16, 25] # ── with condition ────────────────────────────────────────────evens = [x for x in range(20) if x % 2 == 0]print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # ── transform strings ─────────────────────────────────────────words = ["hello", "world", "python"]upper = [w.upper() for w in words]print(upper) # ['HELLO', 'WORLD', 'PYTHON'] # ── filter AND transform ──────────────────────────────────────fruits = ["apple", "Banana", "cherry", "Date", "elderberry"]long_lower = [f.lower() for f in fruits if len(f) > 5]print(long_lower) # ['banana', 'cherry', 'elderberry'] # ── equivalent for-loop (more verbose) ────────────────────────result = []for f in fruits: if len(f) > 5: result.append(f.lower())print(result) # same result # ── nested: flatten a 2D list ─────────────────────────────────matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]flat = [num for row in matrix for num in row]print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Mutability
Mutability determines whether an object can be changed after creation.
Immutable (cannot be modified in place): int, float, complex, bool, str, tuple
Mutable (can be modified in place): list, dict, set
Shared reference trap: assigning a mutable object to two variables makes both point to the same object. Use .copy() (or copy.deepcopy() for nested structures) for an independent copy.
# ── immutable int: reassignment creates a NEW object ─────────x = 10y = xx = 20print(y) # 10 — y unaffected # ── immutable str: methods return a new string ───────────────s = "hello"t = ss = s.upper() # s → "HELLO" (new object)print(t) # "hello" — t unchanged # ── mutable list: shared reference ────────────────────────────a = [1, 2, 3]b = a # b is NOT a copy!b.append(4)print(a) # [1, 2, 3, 4] ← a also changed # ── fix with .copy() ──────────────────────────────────────────c = a.copy()c.append(99)print(a) # [1, 2, 3, 4] ← unchangedprint(c) # [1, 2, 3, 4, 99] # ── mutable dict: same shared-reference behaviour ────────────d1 = {"x": 1}d2 = d1d2["y"] = 2print(d1) # {'x': 1, 'y': 2} ← shared! # ── immutable tuple ───────────────────────────────────────────try: (1, 2, 3)[0] = 99except TypeError as e: print(f"TypeError: {e}") print("Mutable :", ["list", "dict", "set"])print("Immutable:", ["int", "float", "str", "tuple", "bool"])
Practice Questions
Question 1
What is the result of 10 // 3 in Python?
Question 2
What does "Python"[-1] return?
Question 3
What is the output of "abcdef"[1:4]?
Question 4
What does [1, 2, 3].append(4) return?
Question 5
Which of the following will raise a TypeError?
Question 6
How do you safely access a dictionary key that might not exist (without raising KeyError)?
Question 7
What does list(range(2, 10, 3)) produce?
Question 8
What happens when break is encountered inside a for loop?
Question 9
What is printed by: for i in range(5):\n if i == 2: continue\n print(i, end=' ')
Question 10
What is the purpose of 'pass' in Python?
Question 11
What does [x**2 for x in range(1, 6)] produce?
Question 12
Which of the following is immutable in Python?