Reading about Python builds vocabulary. Solving small drills builds fluency. The 30 exercises below cover the six core skill areas every Python beginner needs to develop muscle memory in: strings, numbers, lists, dictionaries, loops, and functions. Each one ships with a hint and a modern Python 3 worked solution, so you can stop staring at a blank screen and start typing. For the wider learning path these drills slot into, see our complete Python beginner guide.

Key Takeaways

  • Drills train one skill at a time and build syntax muscle memory; projects combine many skills and build judgment. Both matter, but skip the drills and you'll stall before the projects start working.
  • Per the Stack Overflow Developer Survey 2024, 49% of professional developers learned to code primarily through self-study, with daily drills as the most common starting tool (Stack Overflow, 2024).
  • Three to five focused drills per day, with 20-minute attempts before peeking at the solution, beats two-hour weekend marathons. The forgetting-curve research from Hermann Ebbinghaus underlies why spaced repetition works (overview of the forgetting curve).
  • Re-solve each exercise from a blank file 3 days later. That's when a solved problem turns into a remembered solution.
  • Move from drills to projects once roughly 80% of the 30 below feel obvious. Earlier and you'll fight your tools; later and you'll plateau.
30
curated drills
across 6 skill areas
~15
minutes median
per drill (first try)
3
days between
solve and re-solve
80%
fluency threshold
to start projects

How Do You Use This Exercise Set?

Pick one category per day and work through its five drills in order. The order inside each category goes from gentle to stretching, so the first exercise warms you up and the fifth one pushes a bit further than feels comfortable. Try the problem cold for up to 20 minutes before reading the hint, then for another 10 before opening the solution. After you read a solution, close the page and re-type it from a blank file. That single habit is what converts reading into knowing. Drills are not projects. A drill trains one skill at a time, like list comprehensions or dictionary lookups, with a sharp definition of "done." A project combines many skills and builds judgment about how they fit together. You need both, and we cover the project side in our 25 Python projects for beginners. For drills specifically, muscle memory comes from quantity at a moderate difficulty, not from one heroic struggle.
Median first-attempt solve time per exercise category (minutes) Median first-attempt solve time, by category CodeGym alumni telemetry, n=1,184 beginner first attempts, 2024-2026 0 8 16 24 32 min 8 Strings 12 Numbers 15 Lists 18 Loops 22 Dicts 28 Functions Category
Time falls roughly in half on the second attempt, and again on the third. That's the drill payoff.

Which String Exercises Build Core Fluency? (5 Drills)

Strings are usually the gentlest place to start. Every exercise here can be done with built-in methods you've already met, so the focus is on chaining them cleanly rather than learning anything new.

1. Reverse a String Without Using Slicing

Given "hello", return "olleh", but without using the [::-1] trick.
Hint: reversed() returns an iterator; "".join() collects it back into a string.
def reverse_string(s: str) -> str:
    return "".join(reversed(s))

assert reverse_string("hello") == "olleh"

2. Count Vowels in a Sentence

Given "Education is the best investment", return 11.
Hint: a generator expression with sum() is one line.
def count_vowels(s: str) -> int:
    return sum(1 for ch in s.lower() if ch in "aeiou")

assert count_vowels("Education is the best investment") == 11

3. Check Whether a Word Is a Palindrome

Given "A man a plan a canal Panama", return True; given "hello", return False.
Hint: strip non-alphanumeric characters and lowercase first, then compare with the reversed copy.
def is_palindrome(s: str) -> bool:
    cleaned = "".join(ch.lower() for ch in s if ch.isalnum())
    return cleaned == cleaned[::-1]

assert is_palindrome("A man a plan a canal Panama")
assert not is_palindrome("hello")

4. Find the Longest Word in a Sentence

Given "the quick brown fox jumps over a lazy dog", return "quick" (or "jumps" — both are length 5).
Hint: max() takes a key= argument.
def longest_word(sentence: str) -> str:
    return max(sentence.split(), key=len)

assert longest_word("the quick brown fox jumps over a lazy dog") in {"quick", "jumps"}

5. Replace Every Space With an Underscore (Without .replace())

Given "convert me to snake case", return "convert_me_to_snake_case" without calling str.replace.
Hint: split on whitespace, then join with the replacement character.
def spaces_to_underscores(s: str) -> str:
    return "_".join(s.split())

assert spaces_to_underscores("convert me to snake case") == "convert_me_to_snake_case"

Which Number and Math Drills Should You Master? (5 Drills)

Number drills exist to make for-loops and the modulo operator feel natural. None of them need math or numpy; the goal is fluent control flow.

6. Sum of Even Numbers From 1 to N

For n = 10, return 30 (the sum 2 + 4 + 6 + 8 + 10).
Hint: range(start, stop, step) takes a step argument.
def sum_evens(n: int) -> int:
    return sum(range(2, n + 1, 2))

assert sum_evens(10) == 30

7. Factorial Without Recursion

For n = 5, return 120, using a plain loop instead of recursion.
Hint: initialize result = 1, then multiply through range(2, n + 1).
def factorial(n: int) -> int:
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

assert factorial(5) == 120
assert factorial(0) == 1

8. First N Terms of the Fibonacci Sequence

For n = 7, return [0, 1, 1, 2, 3, 5, 8].
Hint: two variables and tuple-unpacking: a, b = b, a + b.
def fibonacci(n: int) -> list[int]:
    a, b = 0, 1
    out = []
    for _ in range(n):
        out.append(a)
        a, b = b, a + b
    return out

assert fibonacci(7) == [0, 1, 1, 2, 3, 5, 8]

9. Check Whether a Number Is Prime

Return True for 7 and False for 9.
Hint: you only need to test divisors up to the square root of n.
def is_prime(n: int) -> bool:
    if n < 2:
        return False
    return all(n % i != 0 for i in range(2, int(n ** 0.5) + 1))

assert is_prime(7)
assert not is_prime(9)

10. Round Currency Amounts to Two Decimals

For 19.995, return Decimal("20.00"), not 20.0. Floats accumulate rounding errors and break invoices.
Hint: use decimal.Decimal with ROUND_HALF_UP, not the built-in round().
from decimal import Decimal, ROUND_HALF_UP

def round_currency(amount: float) -> Decimal:
    return Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

assert round_currency(19.995) == Decimal("20.00")
assert round_currency(2.345) == Decimal("2.35")

Which List Drills Cover the Essentials? (5 Drills)

Lists are where idiomatic Python and "Python that just happens to work" start to diverge. The drills below favor the idiomatic version every time.

11. Remove Duplicates While Preserving Order

Given [3, 1, 4, 1, 5, 9, 2, 6, 5, 3], return [3, 1, 4, 5, 9, 2, 6]. Plain set() loses the order.
Hint: dict.fromkeys preserves insertion order since Python 3.7.
def dedupe_preserve_order(items: list) -> list:
    return list(dict.fromkeys(items))

assert dedupe_preserve_order([3, 1, 4, 1, 5, 9, 2, 6, 5, 3]) == [3, 1, 4, 5, 9, 2, 6]

12. Find the Second-Largest Number in a List

For [5, 2, 9, 1, 9, 7], return 7. Note that the two 9s are duplicates of the same value.
Hint: dedupe first, then sort descending, then take index 1.
def second_largest(nums: list[int]) -> int | None:
    unique_sorted = sorted(set(nums), reverse=True)
    return unique_sorted[1] if len(unique_sorted) > 1 else None

assert second_largest([5, 2, 9, 1, 9, 7]) == 7

13. Rotate a List by N Positions

Given [1, 2, 3, 4, 5] and n = 2, return [4, 5, 1, 2, 3].
Hint: slice the tail and head, then concatenate. Watch out when n > len(list).
def rotate(lst: list, n: int) -> list:
    if not lst:
        return lst
    n %= len(lst)
    return lst[-n:] + lst[:-n]

assert rotate([1, 2, 3, 4, 5], 2) == [4, 5, 1, 2, 3]

14. Flatten a Nested List (One Level Only)

Given [[1, 2], [3, 4], [5]], return [1, 2, 3, 4, 5].
Hint: a nested list comprehension reads as "for each sublist, for each item in it".
def flatten_one_level(nested: list[list]) -> list:
    return [item for sub in nested for item in sub]

assert flatten_one_level([[1, 2], [3, 4], [5]]) == [1, 2, 3, 4, 5]

15. Group Consecutive Duplicates Into Run-Length Pairs

Given "aaabbc" as characters, return [("a", 3), ("b", 2), ("c", 1)].
Hint: itertools.groupby handles this without manual state.
from itertools import groupby

def run_length(items) -> list[tuple]:
    return [(key, len(list(group))) for key, group in groupby(items)]

assert run_length("aaabbc") == [("a", 3), ("b", 2), ("c", 1)]

Which Dictionary Drills Show Up Most Often? (5 Drills)

Dictionaries appear in roughly every real-world Python script you'll write, so the drills here matter disproportionately. The collections module does the heavy lifting on three of them.

16. Count Word Frequency in a Paragraph

Given "the cat sat on the mat", return {"the": 2, "cat": 1, "sat": 1, "on": 1, "mat": 1}.
Hint: collections.Counter does exactly this.
from collections import Counter

def word_frequency(text: str) -> dict[str, int]:
    return dict(Counter(text.lower().split()))

assert word_frequency("the cat sat on the mat") == {"the": 2, "cat": 1, "sat": 1, "on": 1, "mat": 1}

17. Merge Two Dictionaries With Summed Values

Given {"a": 2, "b": 3} and {"b": 1, "c": 4}, return {"a": 2, "b": 4, "c": 4}.
Hint: Counter objects add together with the + operator.
from collections import Counter

def merge_summed(a: dict, b: dict) -> dict:
    return dict(Counter(a) + Counter(b))

assert merge_summed({"a": 2, "b": 3}, {"b": 1, "c": 4}) == {"a": 2, "b": 4, "c": 4}

18. Invert a Dictionary (Swap Keys and Values)

Given {"a": 1, "b": 2, "c": 3}, return {1: "a", 2: "b", 3: "c"}.
Hint: a dict comprehension is one clean line. Watch out: duplicate values become collisions.
def invert(d: dict) -> dict:
    return {value: key for key, value in d.items()}

assert invert({"a": 1, "b": 2, "c": 3}) == {1: "a", 2: "b", 3: "c"}

19. Group a List of Dicts by a Key

Given [{"dept": "eng", "name": "Ada"}, {"dept": "eng", "name": "Linus"}, {"dept": "ops", "name": "Grace"}], group by dept.
Hint: collections.defaultdict(list) avoids "key not found" handling.
from collections import defaultdict

def group_by(items: list[dict], key: str) -> dict[str, list]:
    out = defaultdict(list)
    for item in items:
        out[item[key]].append(item)
    return dict(out)

users = [{"dept": "eng", "name": "Ada"}, {"dept": "eng", "name": "Linus"}, {"dept": "ops", "name": "Grace"}]
assert group_by(users, "dept") == {
    "eng": [{"dept": "eng", "name": "Ada"}, {"dept": "eng", "name": "Linus"}],
    "ops": [{"dept": "ops", "name": "Grace"}],
}

20. Find the Key With the Highest Value

Given {"apples": 12, "oranges": 7, "pears": 19}, return "pears".
Hint: max() with key=d.get reads almost as English.
def key_with_max_value(d: dict) -> str:
    return max(d, key=d.get)

assert key_with_max_value({"apples": 12, "oranges": 7, "pears": 19}) == "pears"

Which Loop and Control-Flow Drills Are Worth Practicing? (5 Drills)

Loops feel mechanical once they click, but the click takes a couple of weeks. These five drills are the ones that produce that click.

21. FizzBuzz (Classic Variant)

Print numbers 1 through 15: substitute "Fizz" for multiples of 3, "Buzz" for multiples of 5, and "FizzBuzz" for multiples of both.
Hint: build up a string conditionally; print the string or fall back to the number.
for n in range(1, 16):
    out = ""
    if n % 3 == 0:
        out += "Fizz"
    if n % 5 == 0:
        out += "Buzz"
    print(out or n)

22. Print a Multiplication Table

For n = 7, print "7 x 1 = 7" through "7 x 10 = 70".
Hint: f-strings inside a single for-loop.
def multiplication_table(n: int) -> None:
    for i in range(1, 11):
        print(f"{n} x {i} = {n * i}")

23. Build a Pyramid Pattern With Asterisks

For height = 4, print:
   *
  ***
 *****
*******
Hint: on row i (1-indexed), print height - i spaces and 2*i - 1 asterisks.
def pyramid(height: int) -> None:
    for i in range(1, height + 1):
        print(" " * (height - i) + "*" * (2 * i - 1))

pyramid(4)

24. Find the First Non-Repeating Character in a String

For "swiss", return "w". For "aabb", return None.
Hint: count first with Counter, then scan the original string in order.
from collections import Counter

def first_unique_char(s: str) -> str | None:
    counts = Counter(s)
    return next((ch for ch in s if counts[ch] == 1), None)

assert first_unique_char("swiss") == "w"
assert first_unique_char("aabb") is None

25. Simulate a Dice-Roll Game Until a Target Total

Roll a six-sided die repeatedly, summing the results until the running total hits or exceeds 50. Return the number of rolls used.
Hint: while loop plus random.randint(1, 6).
import random

def rolls_to_target(target: int) -> int:
    total, rolls = 0, 0
    while total < target:
        total += random.randint(1, 6)
        rolls += 1
    return rolls

print(rolls_to_target(50))  # roughly 14-16

Which Function and Algorithm Drills Stretch You Further? (5 Drills)

The last five drills combine the earlier categories. None of them are hard in isolation, but the act of stitching together strings, loops, and dicts inside a single function is the actual goal.

26. Return Both the Average and the Median of a List

Given [3, 1, 4, 1, 5, 9, 2, 6], return (3.875, 3.5).
Hint: statistics.mean and statistics.median are in the standard library.
from statistics import mean, median

def stats(nums: list[float]) -> tuple[float, float]:
    return mean(nums), median(nums)

assert stats([3, 1, 4, 1, 5, 9, 2, 6]) == (3.875, 3.5)

27. Implement Linear Search

Return the index of target in items, or -1 if not present.
Hint: enumerate gives you index and value at once.
def linear_search(items: list, target) -> int:
    for index, item in enumerate(items):
        if item == target:
            return index
    return -1

assert linear_search([10, 20, 30, 40], 30) == 2
assert linear_search([10, 20, 30, 40], 99) == -1

28. Caesar Cipher (Encode and Decode)

Shift each letter forward by n positions in the alphabet, wrapping around at the end. Non-letters stay put.
Hint: use ord and chr; pick the right "A" or "a" base depending on case.
def caesar(text: str, shift: int) -> str:
    out = []
    for ch in text:
        if ch.isalpha():
            base = ord("A") if ch.isupper() else ord("a")
            out.append(chr((ord(ch) - base + shift) % 26 + base))
        else:
            out.append(ch)
    return "".join(out)

assert caesar("Hello, World!", 3) == "Khoor, Zruog!"
assert caesar("Khoor, Zruog!", -3) == "Hello, World!"

29. Recursive Sum of Digits

For 1729, return 19 (which is 1 + 7 + 2 + 9).
Hint: base case is "n is a single digit"; recursive case peels off the last digit with n % 10.
def digit_sum(n: int) -> int:
    if n < 10:
        return n
    return n % 10 + digit_sum(n // 10)

assert digit_sum(1729) == 19
assert digit_sum(0) == 0

30. Custom Sort Using a Key Function

Sort ["apple", "fig", "banana", "kiwi"] by word length ascending, breaking ties alphabetically: ["fig", "kiwi", "apple", "banana"].
Hint: return a tuple from the key function to get multi-level sorting in one pass.
def sort_by_length_then_alpha(words: list[str]) -> list[str]:
    return sorted(words, key=lambda w: (len(w), w))

assert sort_by_length_then_alpha(["apple", "fig", "banana", "kiwi"]) == ["fig", "kiwi", "apple", "banana"]

How Do You Practice These Effectively?

Three habits separate learners who finish the set from learners who quit halfway. None of them are surprising, and that's the point: the surprise is how few people actually do them. Solve without looking, for up to 20 minutes. Stuck past that, read the hint, not the solution. Stuck for another 10, read the solution, close the page, and re-type it from a blank file. Reading code is not the same as writing it; your fingers learn syntax that your eyes only recognize. The first night you type out 30 solutions, you will feel slow. The second night you'll feel less slow, and by the seventh you'll start to feel something close to fluency. Re-solve the same exercise 3 days later. This is the spaced-repetition habit, and it builds on the well-documented forgetting curve described by Ebbinghaus. A problem you solved on Monday and re-solved on Thursday turns into a problem you can solve forever. A problem you solved once on Monday lives for about a week in your head, then fades. The 3-day re-solve is the single highest-impact practice habit you can adopt. Read your own solutions out loud, one week later. Open Monday's file on the following Monday and read it aloud, line by line. Either it makes sense (you've internalized the pattern) or it doesn't (you've forgotten and need another pass). This is faster than re-solving and tells you precisely where your gaps are. Pair it with the 10 common beginner mistakes checklist to catch any anti-patterns you're still typing.

When Should You Move From Exercises to Projects?

The cleanest rule of thumb: when roughly 80% of the 30 drills above feel obvious within their first 5 minutes, you've passed the syntax-fluency threshold. Below that, drills still give you the fastest skill-per-hour return. Above it, projects start to outpace drills because the new skill you need (combining patterns, handling messy real-world inputs) only shows up inside larger programs. "Obvious" doesn't mean "instantly solved." It means you read the problem, your fingers know roughly which functions to reach for, and the work is execution rather than discovery. If half the drills still require Googling a method name, stay with drills for another week or two. For project ideas calibrated to that fluency level, jump to our 25 Python projects for beginners.

Where Can You Get More Python Exercises?

Four reliable, platform-neutral options to extend beyond the 30 above:
  • HackerRank Python Basics. Free, well-graded, gentle difficulty curve. The auto-judge is strict, which trains attention to edge cases.
  • LeetCode (Easy tag, Python language filter). Geared toward interview preparation, but the easies overlap heavily with what you'd see in a junior screen. Skip the mediums until you've finished at least 30 easies.
  • Exercism Python track. Free, with optional human code review. Pace is slower but the feedback quality on a submitted solution is the best of the four.
  • Python.org's official tutorial exercises. Tucked at the end of each chapter. Quietly excellent because they target exactly the concept just introduced (Python tutorial).
If you want auto-graded drills with instant feedback and a gamified progression that keeps you returning daily, CodeGym's Python track is built around the same loop. Every solution gets checked by an AI validator in seconds, and the AI mentor explains the failure when your code doesn't pass. We also cover the broader "learn Python through play" angle in our gamified Python practice spoke.

Run These Drills With Instant AI Feedback

CodeGym's Python track turns the exercises-and-feedback loop into 800+ practical tasks across 62 levels. The AI validator checks every submission in seconds; the AI mentor explains where you went wrong when stuck. First level is free, and the full track is on the pricing page. Open the free Python track →

Should You Use AI Assistants on These Drills?

The short answer is: rarely on drills, sparingly on projects. The point of a drill is to build the neural pathway for one specific Python pattern, and asking ChatGPT to do it for you skips the exact training your brain needs. Save the AI for syntax lookups ("what's the signature of str.split?") and for explanations after you've struggled and want to understand what you missed. Once you move on to projects, AI assistants pay off more, because integrating five libraries is closer to research than to muscle-building.

Frequently Asked Questions

How many Python exercises should I do per day?

Three to five focused drills per day beats twenty rushed ones. Aim for 45-60 minutes of practice, alternating between solving fresh problems and re-solving older ones. Quality of focus during each attempt matters more than raw count, and short daily sessions outperform marathon weekend sessions because spaced repetition is how long-term memory forms.

Is it OK to look at the solution if I can't solve one?

Yes, but only after 20 minutes of honest attempts. Read the solution, close the page, then re-implement it from scratch without peeking. The act of typing the working code yourself is what trains your fingers. Tomorrow, solve the same exercise from a blank file, and you'll discover whether the lesson actually stuck.

Are LeetCode-style exercises useful for beginners?

LeetCode easies are useful once you can fluently write loops, functions, and dictionary lookups. Before that, they're discouraging and skip too many fundamentals. Start with drills like the 30 above to build syntax fluency, then graduate to LeetCode easies once you can solve them without consulting documentation. Junior interview prep (our Python interview Q&A spoke goes deeper) typically asks for easy-level proficiency only.

Should I time myself on Python exercises?

Not in your first three months. Speed comes from repetition, not from artificial pressure. Once you can solve an exercise correctly without help, then add a soft time cap on a re-solve as a fluency check. For interview prep specifically, time yourself only on the last two weeks before the screen, to build comfort with the format.

The Bottom Line: Drills Build Fluency, Then Projects Take Over

Thirty drills, three to five a day, with a 3-day re-solve loop, will move you from "I read Python" to "I write Python" inside a single month. The drills aren't the destination; they're the bridge that makes projects feel possible. Once 80% of these feel obvious, jump to the 25 Python projects for beginners, and for the full learning arc from first print to first paid role, start with our complete Python beginner guide

Learn more about our mission and terms of service. Published article was last reviewed on 2026-05-14.