A coding challenge is a small, precisely defined problem that tests whether you can turn a specification into correct, efficient code. They appear in interviews, competitive programming and on practice sites, and they're an excellent way to strengthen the fundamentals from this course.
This page works through four classic challenges with full solutions: repeated characters, balanced brackets, merging intervals and shortest unweighted routes. More importantly, it shows a repeatable method for any challenge:
- Pin down the contract: inputs, outputs and what happens with invalid input.
- Write test cases first, including boundaries and cases designed to break shortcuts.
- Build a simple working solution, then improve it if needed.
- Explain why it works (its invariant) and what it costs (its complexity).
The challenges use a summer festival's badge printing, schedules and site map as their setting.
Challenge contracts
Before writing code, answer every question the problem statement leaves open. For "return the characters that appear more than once in a badge text":
- Are upper- and lower-case letters the same character?
- Do spaces and punctuation count?
- What order should results be in, and should each character appear once?
- What happens for an empty string?
Write the decisions down as a docstring:
def repeated_symbols(text):
"""Return each character that appears more than once in text,
case-sensitive, spaces included, sorted, each listed once.
An empty string returns []."""Ambiguous rules make correctness impossible to judge: two people can write "correct" solutions that disagree. In an interview, asking these questions out loud is part of what's being assessed. On practice sites, the examples usually settle them, so read every example before coding.
Test cases
Good tests cover three kinds of input:
- Ordinary: a typical case from the problem statement.
- Boundaries: empty input, one element, everything identical, the largest allowed size.
- Adversarial: cases designed to break a tempting shortcut.
For balanced brackets, the tempting shortcut is "count opening and closing brackets and check the counts match". The adversarial test ")(" has matching counts but isn't balanced, and "([)]" has every pair present but in the wrong nesting order:
cases = {
"": True, "()": True, "([]{})": True, # ordinary and boundary
")(": False, "([)]": False, "((": False, # adversarial
}Write these before the solution, so you're testing the specification, not the code you happened to write. Then a failing case tells you precisely which part of the reasoning is wrong.
Incremental solutions
Start with the clearest solution you're confident is correct, verify it against the tests, and only then optimise if the constraints require it. For merging overlapping intervals:
def merge(intervals):
merged = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return mergedSorting first is the key insight: once intervals are ordered by start time, each one can only overlap the most recent merged interval. That turns an O(n²) "compare everything with everything" approach into O(n log n).
Optimising before the contract and tests are clear causes avoidable mistakes, such as treating [9, 11] and [11, 12] as overlapping or not. Decide that in the contract first (here, touching intervals merge because the comparison is <=), then write code that follows it.
Explanation
A finished solution includes a short explanation of why it's correct and what it costs. For repeated characters with a dictionary:
- Invariant: after processing the first i characters,
counts[c]equals the number of times c appears among them. - Correctness: at the end, the invariant covers the whole string, so filtering counts above 1 gives exactly the repeated characters.
- Cost: O(n) time for one pass, plus O(k log k) to sort the k distinct characters; O(k) extra space.
An invariant is a statement that's true before the loop, stays true after each iteration, and gives the answer when the loop ends. For balanced brackets the invariant is "the stack holds exactly the opening brackets not yet closed, in order". Passing the examples shows the code works on those inputs; the invariant explains why it works on all of them.
Independent practice and a worked check
Task: solve repeated symbols, balanced brackets, merged intervals and shortest unweighted routes, documenting contracts and costs. Use this harness to test your own versions:
def check(func, cases):
failures = [(given, expected, func(*given)) for given, expected in cases
if func(*given) != expected]
for given, expected, got in failures:
print(f"FAIL {func.__name__}{given}: expected {expected}, got {got}")
print(f"{func.__name__}: {len(cases) - len(failures)}/{len(cases)} passed")
def repeated_symbols(text):
counts = {}
for ch in text:
counts[ch] = counts.get(ch, 0) + 1
return sorted(ch for ch, n in counts.items() if n > 1)
check(repeated_symbols, [(("",), []), (("abc",), []), (("aAa",), ["a"]), (("a a",), ["a"])])This prints repeated_symbols: 4/4 passed. Note that "a a" contains one space, so only "a" repeats. Write each function's contract and complexity in its docstring. For the worked check, a challenge should include boundary and adversarial cases besides an ordinary example.
Reference and next practice
The collections and bisect modules provide building blocks used in many challenge solutions. Continue with practice problems and revisit interview questions and complexity. Practise with the Python exercises and Python quiz.
Python Coding Challenges Summary
- Challenge contracts: State input types, expected outputs and invalid input behavior before solving.
- Test cases: Include ordinary cases, boundaries and a case designed to break a tempting shortcut.
- Incremental solutions: A clear working approach can be improved after its behavior is verified.
- Explanation: A challenge solution should explain its invariant and complexity.
Python Coding Challenges Examples
Each example shows the code, the output it produces and what each part does.
Python Coding Challenges in a small program
Challenge 1, repeated characters, solved with a counting dictionary:
def repeated_symbols(text):
counts = {}
for symbol in text:
counts[symbol] = counts.get(symbol, 0) + 1
return sorted(symbol for symbol, count in counts.items() if count > 1)
print(repeated_symbols("lantern trail"))['a', 'l', 'n', 'r', 't']Line-by-line explanation
- Lines 3 to 4One pass counts every character, including the space.
- Line 5Only characters seen more than once are kept, sorted for a predictable result.
Challenge 2: balanced brackets with a stack
def balanced(text):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in text:
if ch in "([{":
stack.append(ch)
elif ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
return not stack
cases = {"": True, "()": True, "([]{})": True, ")(": False, "([)]": False, "((": False}
for text, expected in cases.items():
assert balanced(text) == expected, text
print("all", len(cases), "bracket cases pass")all 6 bracket cases passLine-by-line explanation
- Lines 5 to 6Every opening bracket is pushed; the stack holds brackets still waiting to close.
- Lines 8 to 9A closing bracket must match the most recent unclosed opener, which is why
"([)]"fails. - Line 10Anything left on the stack was never closed.
Challenge 3: merge overlapping time slots
def merge(intervals):
merged = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
print(merge([[14, 16], [9, 11], [10, 12], [16, 17], [19, 20]]))
print(merge([]))
print(merge([[9, 18], [10, 11]]))[[9, 12], [14, 17], [19, 20]]
[]
[[9, 18]]Line-by-line explanation
- Line 3Sorting by start time means each interval can only overlap the last merged one.
- Lines 4 to 5
<=merges touching slots such as 14-16 and 16-17, as the contract decided. - Line 12A slot fully inside another is absorbed, because
maxkeeps the later end.
Challenge 4: fewest steps between festival stages
from collections import deque
paths = {"Gate": ["Food", "Main"], "Food": ["Gate", "Acoustic"], "Main": ["Gate", "Dance"],
"Acoustic": ["Food", "Dance"], "Dance": ["Main", "Acoustic"], "Camp": []}
def fewest_steps(graph, start, goal):
steps = {start: 0}
queue = deque([start])
while queue:
place = queue.popleft()
if place == goal:
return steps[place]
for nxt in graph[place]:
if nxt not in steps:
steps[nxt] = steps[place] + 1
queue.append(nxt)
return None
print(fewest_steps(paths, "Gate", "Dance"))
print(fewest_steps(paths, "Gate", "Camp"))
print(fewest_steps(paths, "Food", "Food"))2
None
0Line-by-line explanation
- Lines 7 to 17Breadth-first search visits places in order of distance, so the first time it reaches the goal is via the fewest steps.
- Lines 20 to 21An unreachable goal returns
None, and start equal to goal returns 0: both boundary cases from the contract.
Common Python Coding Challenges Mistakes
Counting brackets instead of matching them
def balanced(text):
return text.count("(") == text.count(")") # ")(" passesdef balanced(text):
depth = 0
for ch in text:
depth += 1 if ch == "(" else -1 if ch == ")" else 0
if depth < 0:
return False # closed before opened
return depth == 0Testing only the example from the problem
assert merge([[1, 3], [2, 4]]) == [[1, 4]] # one ordinary casefor given, expected in [([], []), ([[1, 2]], [[1, 2]]), ([[1, 2], [2, 3]], [[1, 3]]),
([[5, 6], [1, 2]], [[1, 2], [5, 6]]), ([[1, 10], [2, 3]], [[1, 10]])]:
assert merge(given) == expected, givenPython Coding Challenges in Real Projects
Where these challenges appear
Each challenge here is a pattern you'll meet in real software. Counting with dictionaries underlies analytics and deduplication. Bracket matching is how editors, linters and parsers check code and markup. Interval merging handles calendar availability, booking systems and time-range queries. Breadth-first search powers routing, network analysis and "people you may know" features.
Interview platforms and sites such as Exercism, Advent of Code and LeetCode offer hundreds more challenges. Using the four-step method on each, rather than memorising solutions, is what builds lasting skill.
Python Coding Challenges FAQ
How do I get better at Python coding challenges?
Solve problems regularly, starting with easy ones, and follow the same method each time: clarify the contract, write tests, build a simple solution, then improve and explain it. Review other people's solutions afterwards to learn new patterns.
Should I memorise solutions to common problems?
Memorise patterns, not solutions: counting with a dictionary, two pointers, sorting first, using a stack, breadth-first search. Recognising which pattern fits a new problem is the real skill.
What data structures appear most in coding challenges?
Lists, dictionaries and sets most often, then stacks and queues (deque), heaps (heapq) and graphs as adjacency lists. Knowing their operation costs lets you pick the right one quickly.
How do I know if my solution is efficient enough?
Compare its complexity with the input limits. Around 10⁷ to 10⁸ simple operations per second is a rough guide, so with n up to 100,000, an O(n²) solution is usually too slow while O(n log n) is fine.
Are coding challenges the same as real programming work?
Not exactly. Real work involves unclear requirements, existing code, collaboration and maintenance. But the skills challenges train, precise thinking, testing edge cases and choosing data structures, transfer directly.
Python Coding Challenges Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What should a challenge include besides an ordinary example?
Which review concern is valid for Python coding challenges?
Python Coding Challenges Quiz
2 questions to check what you learned.
What should a challenge include besides an ordinary example?
- Only a time limit
- Boundary and adversarial cases
- Python ignores the distinction
Which statement correctly describes incremental solutions?
- A clear working approach can be improved after its behavior is verified.
- Only the example output matters, not the rule
- Every operation has the same behavior