Python Exercises to Sharpen Your Programming Logic
A hands-on collection of Python exercises to train your problem-solving mindset — from basic logic to recursion, data structures, and algorithms. With full solutions and explanations.
Writing Python code every day is not the same as thinking like a programmer. Logic, pattern recognition, and algorithmic thinking are muscles — they need deliberate training. This post is a curated set of exercises, from beginner to intermediate, with real code and explanations. Work through them at your own pace.
Why Exercises Matter More Than Tutorials
Tutorials give you comfort. Exercises give you strength.
When you watch someone solve a problem, your brain says “I get it.” When you solve it, your brain says “I own it.” There’s no shortcut. The only way to get better at problem-solving is to solve problems.
Let’s get to it.
🟢 Level 1 — Foundations
Exercise 1: FizzBuzz (Classic, but don’t skip it)
Print numbers from 1 to 100. For multiples of 3 print "Fizz", for multiples of 5 print "Buzz", for multiples of both print "FizzBuzz".
The naive version:
for i in range(1, 101):
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
The Pythonic version:
for i in range(1, 101):
print("Fizz" * (i % 3 == 0) + "Buzz" * (i % 5 == 0) or i)
Key insight: Boolean values in Python are integers (
True == 1,False == 0). Multiplying a string by0gives an empty string, which is falsy — soor ikicks in.
Exercise 2: Palindrome Checker
Write a function that returns True if a string is a palindrome (reads the same forwards and backwards), ignoring spaces and case.
def is_palindrome(s: str) -> bool:
cleaned = s.replace(" ", "").lower()
return cleaned == cleaned[::-1]
# Tests
assert is_palindrome("racecar") == True
assert is_palindrome("A man a plan a canal Panama") == True
assert is_palindrome("hello") == False
print("All tests passed ✅")
Challenge variant: What if you can’t use slicing? Implement it with two pointers.
def is_palindrome_two_pointers(s: str) -> bool:
cleaned = s.replace(" ", "").lower()
left, right = 0, len(cleaned) - 1
while left < right:
if cleaned[left] != cleaned[right]:
return False
left += 1
right -= 1
return True
Why two pointers? This is
O(n)time andO(1)extra space — useful when working with very large strings or when learning for interviews.
Exercise 3: Count Word Frequency
Given a sentence, return a dictionary with each word and how many times it appears.
from collections import Counter
def word_frequency(sentence: str) -> dict:
words = sentence.lower().split()
return dict(Counter(words))
text = "the cat sat on the mat the cat"
print(word_frequency(text))
# {'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1}
Without Counter (manual approach):
def word_frequency_manual(sentence: str) -> dict:
frequency = {}
for word in sentence.lower().split():
frequency[word] = frequency.get(word, 0) + 1
return frequency
Lesson:
dict.get(key, default)is cleaner than checkingif key in dict. Learn it, love it.
Exercise 4: Find Duplicates in a List
Given a list of integers, return only the elements that appear more than once.
def find_duplicates(nums: list) -> list:
seen = set()
duplicates = set()
for num in nums:
if num in seen:
duplicates.add(num)
seen.add(num)
return list(duplicates)
print(find_duplicates([1, 2, 3, 2, 4, 3, 5])) # [2, 3]
print(find_duplicates([1, 1, 1, 2])) # [1]
print(find_duplicates([1, 2, 3])) # []
Why sets? Lookup in a set is
O(1). If you used a list forseen, each lookup would beO(n), making the whole functionO(n²).
🟡 Level 2 — Intermediate Logic
Exercise 5: Flatten a Nested List
Write a function that takes an arbitrarily nested list and returns a flat list.
def flatten(nested: list) -> list:
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
print(flatten([1, [2, 3], [4, [5, 6]], 7]))
# [1, 2, 3, 4, 5, 6, 7]
print(flatten([[[1]], [2, [3, [4]]]]))
# [1, 2, 3, 4]
One-liner with generators:
def flatten_gen(nested):
for item in nested:
if isinstance(item, list):
yield from flatten_gen(item)
else:
yield item
print(list(flatten_gen([1, [2, [3, [4]]]]))) # [1, 2, 3, 4]
Recursion mindset: When a problem contains itself (a list inside a list), recursion is your friend. The base case is “item is not a list”.
Exercise 6: Binary Search
Implement binary search from scratch. Given a sorted list and a target value, return the index of the target or -1 if not found.
def binary_search(arr: list, target: int) -> int:
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # target is in the right half
else:
right = mid - 1 # target is in the left half
return -1
nums = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(nums, 23)) # 5
print(binary_search(nums, 10)) # -1
print(binary_search(nums, 2)) # 0
print(binary_search(nums, 91)) # 9
Why does this matter? Linear search is
O(n). Binary search isO(log n). On a list of 1 million items, binary search takes at most ~20 comparisons. Linear search could take 1,000,000.
Exercise 7: Anagram Groups
Given a list of words, group words that are anagrams of each other.
from collections import defaultdict
def group_anagrams(words: list[str]) -> list[list[str]]:
groups = defaultdict(list)
for word in words:
key = tuple(sorted(word)) # "eat" → ('a', 'e', 't')
groups[key].append(word)
return list(groups.values())
words = ["eat", "tea", "tan", "ate", "nat", "bat"]
result = group_anagrams(words)
for group in result:
print(group)
# ['eat', 'tea', 'ate']
# ['tan', 'nat']
# ['bat']
Key insight: Two words are anagrams if and only if their sorted characters are identical. Sorting acts as a canonical form (a “fingerprint”) for the group.
Exercise 8: Valid Parentheses
Given a string containing only (, ), {, }, [, ], determine if the input string is valid. A string is valid if every open bracket has a matching close bracket in the correct order.
def is_valid_parentheses(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
# It's a closing bracket
top = stack.pop() if stack else '#'
if mapping[char] != top:
return False
else:
# It's an opening bracket
stack.append(char)
return len(stack) == 0
print(is_valid_parentheses("()")) # True
print(is_valid_parentheses("()[]{}")) # True
print(is_valid_parentheses("(]")) # False
print(is_valid_parentheses("([)]")) # False
print(is_valid_parentheses("{[]}")) # True
Stack intuition: A stack is a Last-In-First-Out structure. Every time you see an opening bracket, push it. Every time you see a closing bracket, check if it matches the last opened one (the top of the stack). This is the classic pattern for bracket validation, HTML parsing, and expression evaluation.
Exercise 9: Caesar Cipher
Implement a Caesar cipher — shift each letter in a string by n positions in the alphabet. Wrap around from z back to a.
def caesar_cipher(text: str, shift: int) -> str:
result = []
for char in text:
if char.isalpha():
base = ord('A') if char.isupper() else ord('a')
shifted = (ord(char) - base + shift) % 26 + base
result.append(chr(shifted))
else:
result.append(char) # Keep spaces, punctuation as-is
return "".join(result)
print(caesar_cipher("Hello, World!", 3)) # "Khoor, Zruog!"
print(caesar_cipher("Khoor, Zruog!", -3)) # "Hello, World!"
print(caesar_cipher("xyz", 3)) # "abc" (wraps around)
ord()andchr():ord('a')gives you the ASCII integer value of a character (97).chr(97)gives you back'a'. The modulo% 26ensures you wrap around the alphabet. This pattern is essential for any character manipulation.
🔴 Level 3 — Algorithms & Data Structures
Exercise 10: Fibonacci — Four Ways
Calculate the nth Fibonacci number. Each approach teaches something different.
# 1. Naive recursion — O(2^n) — beautiful but slow
def fib_recursive(n: int) -> int:
if n <= 1:
return n
return fib_recursive(n - 1) + fib_recursive(n - 2)
# 2. Memoization — O(n) time, O(n) space
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memo(n: int) -> int:
if n <= 1:
return n
return fib_memo(n - 1) + fib_memo(n - 2)
# 3. Bottom-up dynamic programming — O(n) time, O(n) space
def fib_dp(n: int) -> int:
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# 4. Space-optimized — O(n) time, O(1) space ✅ Best
def fib_optimized(n: int) -> int:
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
# All produce the same result
for i in range(10):
print(fib_optimized(i), end=" ")
# 0 1 1 2 3 5 8 13 21 34
Why learn four ways? Each version teaches a concept: naive recursion → the problem structure; memoization → caching subproblems; DP table → building from the bottom; space optimization → only keeping what you need. This progression is exactly how you think through any DP problem.
Exercise 11: Linked List — Build from Scratch
Implement a basic singly linked list with append, prepend, delete, and print methods.
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
"""Add to the end — O(n)"""
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def prepend(self, data):
"""Add to the beginning — O(1)"""
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete(self, data):
"""Remove first occurrence of data — O(n)"""
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next:
if current.next.data == data:
current.next = current.next.next
return
current = current.next
def to_list(self) -> list:
result = []
current = self.head
while current:
result.append(current.data)
current = current.next
return result
def __repr__(self):
return " → ".join(str(x) for x in self.to_list())
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.prepend(0)
print(ll) # 0 → 1 → 2 → 3
ll.delete(2)
print(ll) # 0 → 1 → 3
Why linked lists? Understanding pointers and node traversal is fundamental for trees, graphs, and interview problems. Python’s
listhides all of this complexity — building it yourself makes it click.
Exercise 12: Two Sum
Given a list of integers and a target, return the indices of two numbers that add up to the target. Assume exactly one solution exists.
def two_sum(nums: list[int], target: int) -> tuple[int, int]:
seen = {} # value → index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
print(two_sum([2, 7, 11, 15], 9)) # (0, 1) → 2 + 7 = 9
print(two_sum([3, 2, 4], 6)) # (1, 2) → 2 + 4 = 6
print(two_sum([3, 3], 6)) # (0, 1)
The pattern: For every number, you need
target - num. Store what you’ve seen so far in a dictionary. First time you find a complement that you’ve already seen — you’re done. This reduces a brute-forceO(n²)toO(n).
Exercise 13: Matrix Spiral Traversal
Given an m × n matrix, return all elements in spiral order (clockwise from the outside in).
def spiral_order(matrix: list[list[int]]) -> list[int]:
result = []
if not matrix:
return result
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
# Move right across the top row
for col in range(left, right + 1):
result.append(matrix[top][col])
top += 1
# Move down the right column
for row in range(top, bottom + 1):
result.append(matrix[row][right])
right -= 1
# Move left across the bottom row
if top <= bottom:
for col in range(right, left - 1, -1):
result.append(matrix[bottom][col])
bottom -= 1
# Move up the left column
if left <= right:
for row in range(bottom, top - 1, -1):
result.append(matrix[row][left])
left += 1
return result
matrix = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]
print(spiral_order(matrix))
# [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
Shrinking boundaries: Use four pointers (
top,bottom,left,right) that converge inward after each traversal. This pattern of “shrinking bounds” also applies to other matrix problems.
🧠 The Mental Models Behind These Exercises
Each exercise targets a specific thinking pattern:
| Exercise | Pattern | Use when… |
|---|---|---|
| FizzBuzz | Modular arithmetic | Divisibility, cycles |
| Palindrome | Two pointers | Comparing two ends |
| Word Frequency | Hash map counting | Frequencies, counts |
| Find Duplicates | Set membership | Seen/not-seen tracking |
| Flatten | Recursion | Problem contains itself |
| Binary Search | Divide and conquer | Sorted data, O(log n) |
| Anagram Groups | Canonical keys | Grouping by signature |
| Valid Parentheses | Stack | Matching pairs, nesting |
| Caesar Cipher | Modular arithmetic + ord/chr | Character encoding |
| Fibonacci | Dynamic programming | Overlapping subproblems |
| Linked List | Pointer manipulation | Manual data structures |
| Two Sum | Hash map complement | Pair-finding |
| Spiral | Shrinking boundaries | Matrix traversal |
How to Practice Effectively
- Try before looking at the solution. Even 10 minutes of struggle beats reading immediately.
- Solve it multiple ways. The brute-force first, then optimize.
- Verbalize your thinking. Pretend you’re explaining it to someone. This forces clarity.
- Test edge cases manually. Empty input, single element, all duplicates, negative numbers.
- Revisit problems you failed. After 48 hours, try again from scratch.
“The first time you solve a problem, you learn the solution. The second time, you learn the pattern. The third time, it’s yours.”
Recommended Next Steps
Once you’re comfortable with all the exercises above, move toward:
- LeetCode Easy → Medium (start with arrays, hashmaps, strings)
- Sorting algorithms: implement bubble, merge, and quicksort yourself
- Tree traversal: BFS, DFS on binary trees
- Graph problems: connected components, shortest path
The goal isn’t to memorize solutions. It’s to train your brain to see patterns and reach for the right tool instinctively.
Happy coding. 🐍