Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Recursion, Searching & Sorting  ›  Lesson

Recursion & Nested Playlists

Recursion 20 minA function that solves a problem by calling itself on a smaller piece
You're building a piece ofTunebox — search & charts
This piece — flatten_playlist(): Playlists inside playlists — flattened to a running order.
Scenario A listener's "Road Trip" folder holds "Morning" and "Night" playlists, and "Night" holds "Late". Before Tunebox can press play it needs one running order — recursion walks the whole tree to build it.
Your task
Build flatten_playlist(node). A playlist is a list whose items are either track titles (strings) or nested playlists (lists), nested to any depth. Return one flat list of every title, in play order. Example: flatten_playlist(["a", ["b", "c"], "d"]) → ["a", "b", "c", "d"].

Recursion & Nested Playlists

Tunebox lets people put playlists inside playlists. "Road Trip" holds "Morning" and "Night", "Night" holds "Late", and nobody promised it stops there. One loop handles one level; two nested loops handle two. A recursive function handles any depth — it deals with one level and hands every sub-playlist to itself.

Two cases: stop, or shrink

Every recursive function has two parts:

  • a base case — an input small enough to answer directly, with no further call;
  • a recursive case — do a little work, then call the same function on a smaller input.
python

The smaller call has to move toward the base case. Leave the base case out and nothing ever stops it:

What happens when you run this?
def tracks_left(n):
    return 1 + tracks_left(n - 1)

print(tracks_left(3))

The call stack

Each call gets its own frame — its own private n — and the caller waits on the line that made the call. Frames pile onto the call stack on the way down and come off it, one at a time, on the way back up.

step through it
1def factorial(n):
2 if n <= 1:
3 return 1
4 return n * factorial(n - 1)
5 
6result = factorial(3)
7print(result)

Step through it. factorial(3) can't finish 3 * … until factorial(2) returns, and that waits on factorial(1). Only the base case returns without waiting — then the answers flow back up: 1, then 2 * 1, then 3 * 2.

That also means code placed after the recursive call runs on the way back up:

What does this print?
def echo(n):
    if n == 0:
        return
    print(n, end=" ")
    echo(n - 1)
    print(n, end=" ")

echo(3)

❓ Cross-question — "Why doesn't factorial(2) overwrite the n that factorial(3) is still using?" Every call has its own local variables in its own frame. The n in one frame and the n in the next are different variables that share a name — exactly why two ordinary calls to any function never clash.

Depth, and Python's limit

Recursion depth is how many frames are on the stack at once. Python caps it — 1000 by default — so a runaway fails with a clear error instead of crashing.

python

Python also has no tail-call optimisation: even a call that is the very last thing a function does still costs a frame. So recursion that goes one level deeper per item — walking a 10,000-track list one track at a time — will hit the limit. Recursion that goes one level deeper per level of nesting won't: nobody nests playlists a thousand deep.

Coming from Java/JS: Java throws StackOverflowError, JS throws RangeError: Maximum call stack size exceeded. Same idea — but Python's limit is deliberately low. sys.setrecursionlimit() can raise it; needing to is usually a sign the problem wanted a loop.

Tree-shaped recursion

A nested playlist is a tree. Every item is either a title (a leaf) or a sub-playlist (a branch). The function answers leaves directly and hands branches to itself:

python

Where's the base case? There is no if for it. A playlist of plain titles makes no recursive call, so its loop just ends and returns — the base case is hiding in the shape of the data.

Some recursions branch into two calls every time, and the work explodes:

What does this print?
calls = 0

def fib(n):
    global calls
    calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

fib(5)
print(calls)

Flattening is the same walk, collecting titles instead of counting them. This version is one word away from right:

broken — fix it

This should print one flat list of four titles. It prints the sub-playlist as a list nested inside the result.

Recursion vs iteration

Anything recursion does, a loop can do — with an explicit stack standing in for the call stack:

python
the problem is… reach for
a straight line — count down, walk a list a loop
branching or nested — playlists, folders, JSON, trees recursion
nested and possibly very deep (input you don't control) a loop with an explicit stack

❓ Cross-question — "Isn't recursion slower?" A little — each call pays for a frame. That overhead is rarely what matters. What matters is the shape of the work, and that's what the table below counts.

function time extra space
countdown(n), factorial(n) O(n) O(n) — n frames
naive fib(n) O(2ⁿ) O(n) — the deepest chain
count_tracks(playlist) O(t) — every item once O(d) — d = nesting depth
flatten with extend O(t · d) worst case — each level copies titles up O(t + d)

Idioms & real-world patterns

Pass the result down instead of copying it up

out.extend(flatten(item)) copies each title once per level it climbs. A helper that appends into one shared list does no copying at all:

python

Don't recurse on slices

plays[1:] builds a brand-new list, so plays[0] + total(plays[1:]) is O(n²) and n frames deep. Pass an index instead — or admit it's a loop:

python

⚡ Advanced — recursive generators with yield from

A generator can recurse too. yield from passes along everything the inner call yields, so titles stream out without building a list at every level:

python

⚡ Advanced — remember answers with lru_cache

Naive fib recomputes the same calls again and again. functools.lru_cache stores each result the first time, turning O(2ⁿ) into O(n) — the first step toward dynamic programming:

python

🎯 Your turn

Write flatten_playlist(node). A playlist is a list whose items are titles (strings) or nested playlists (lists), to any depth. Return one flat list of titles in play order:

  • flatten_playlist(["a", ["b", "c"], "d"])["a", "b", "c", "d"]
  • flatten_playlist([])[]

Hint — loop the items: a list means recurse and extend, anything else is a title to append.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Tunebox is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upCountdown to play

Write countdown_list(n) returning [n, n - 1, …, 1] recursively — no loops, no range. countdown_list(0) is [].

base-caseshrink-input
DrillCounting DJ sets

A DJ fills an n-minute slot with 1-minute stingers and 2-minute loops, in any order. Write set_builds(n) returning how many different orderings fill exactly n minutes, recursively. set_builds(0) is 1 (the empty set) and set_builds(3) is 3 (1+1+1, 1+2, 2+1).

two-base-casestree-recursion
BuildNested playlist length

Write total_seconds(node). A playlist is a list whose items are either track dicts like {"title": "Dusk", "seconds": 200} or nested playlists (lists), to any depth. Return the total length in seconds.

tree-recursiondict
BossHow deep does it go?

Write playlist_depth(node) returning how deeply a playlist is nested. The top playlist counts as depth 1, and every level of sub-playlist inside it adds 1. Titles are strings; sub-playlists are lists. ["a", "b"] → 1, ["a", ["b", ["c"]]] → 3, and an empty playlist [] → 1.

tree-recursionbase-case
CapstoneWhere is that track?

Write find_path(node, title) returning the index path to the first place title appears in a nested playlist, searching in play order. The path lists the index at each level: in ["a", ["b", ["c"]]], "c" is at [1, 1, 0] and "a" is at [0]. If the title isn't there, return [].

tree-recursionrecursionlists
flatten_playlist.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
flatten_playlist(node) → listEvery title from a nested playlist, flattened into play order.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.