How Fibonacci DP Works
Cache the sequence on a class instance, extend it on demand, return a slice. The canonical example of memoization replacing exponential recursion.
What you will learn
- Why the naive recursive Fibonacci is O(2^n) and memoization reduces it to O(n)
- How the Fibonacci class stores the growing sequence on self.sequence and extends it incrementally
- What the walrus operator assignment computes and why it determines whether extension is needed
- How returning self.sequence[:index] delivers the full prefix rather than a single Fibonacci number
- How two doctests pin the return shape for index 10 and index 5
- How the interactive main loop reuses the same Fibonacci instance across calls for O(1) lookups
Prerequisites
- Comfort reading Python classes and list indexing
- No prior knowledge of dynamic programming required
The story behind Fibonacci DP
Leonardo of Pisa, known as Fibonacci, introduced the sequence in his 1202 book Liber Abaci (Book of Calculation) as a model of rabbit population growth under idealized breeding assumptions. Each generation produces as many offspring as the prior two combined, which gives the recurrence F(n) = F(n-1) + F(n-2). The sequence itself was known in Indian mathematics centuries earlier, but Fibonacci's book brought it to Western Europe.
The naive recursive implementation of that recurrence runs in O(2^n) time because every call to F(n) spawns two calls, and most subproblems are recomputed exponentially many times. The fix is memoization: record each computed value so that any future call for the same argument returns immediately. With memoization the recurrence costs O(n) time and O(n) space, which is the definition of dynamic programming applied to this problem.
A further reduction is possible. Because F(n) depends only on F(n-1) and F(n-2), you can compute the sequence with O(1) space by keeping only the last two values. Binet's formula, derived from the golden ratio, gives a closed-form result in O(log n) via fast matrix exponentiation. This implementation uses the class-based memoization approach because it makes the growth of the cached sequence visible and the code straightforward to read.
Module Docstring: The Educational Framing
dynamic_programming/fibonacci.py:1The module docstring frames the file as a DP solution to the Fibonacci sequence problem, setting expectations before any code.
The module docstring on lines 2 and 3 does the work the contribution checklist requires: it names the implementation approach (dynamic programming) and the problem (Fibonacci sequence) in one sentence. That framing distinguishes this file from maths/fibonacci.py, which exists in the same repository and provides multiple variants including Binet's formula and iterative approaches. The DP label is load-bearing: a reader opening this file expecting an iterative or closed-form solution will find a memoized class instead. The framing also tells a learner where to look next if they want to compare approaches. Browse maths/fibonacci.py for Binet's formula and the pure-iterative variant; come here for the memoized class that caches across calls.
The module docstring names the approach and the problem. That one sentence separates this file from the five other Fibonacci implementations in the repository.
"""
This is a pure Python implementation of Dynamic Programming solution to the fibonacci
sequence problem.
"""The Fibonacci Class: Instance State as the Cache
dynamic_programming/fibonacci.py:7The class initializes self.sequence to [0, 1] and the get method extends the list on demand, so the cache grows with each call.
The cache is self.sequence, initialized to [0, 1]. Every call to get(index) checks whether the requested index is already covered and appends only the missing terms. The return value is a list prefix, not a single Fibonacci number: get(10) returns the first ten Fibonacci numbers starting from index 0. Two doctests pin the return shape. Fibonacci().get(10) returns [0, 1, 1, 2, 3, 5, 8, 13, 21, 34], which is ten elements. Fibonacci().get(5) returns [0, 1, 1, 2, 3], which is five elements. Each doctest creates a fresh instance with Fibonacci(), so the cache starts at [0, 1] for both. In the main function below, a single instance is reused across multiple calls, making any repeat lookup O(1).
The class cache starts at [0, 1] and grows on demand. Returning a list prefix means get(n) delivers every Fibonacci number up to index n at once.
class Fibonacci:
def __init__(self) -> None:
self.sequence = [0, 1]
def get(self, index: int) -> list:
"""
Get the Fibonacci number of `index`. If the number does not exist,
calculate all missing numbers leading up to the number of `index`.
>>> Fibonacci().get(10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> Fibonacci().get(5)
[0, 1, 1, 2, 3]
"""
if (difference := index - (len(self.sequence) - 2)) >= 1:
for _ in range(difference):
self.sequence.append(self.sequence[-1] + self.sequence[-2])
return self.sequence[:index]The Walrus Operator: Computing the Gap
dynamic_programming/fibonacci.py:21The walrus assignment computes how many terms are missing and also controls whether the extension loop runs at all.
Python 3.8 introduced the walrus operator (:=) for assignment inside expressions, and this line shows the canonical use: compute a value, assign it to a name, and test it in one expression. The expression index - (len(self.sequence) - 2) computes how many Fibonacci numbers the list is missing to satisfy the request. The subtraction of 2 accounts for the fact that the last valid index in a list of length k is k - 1, and the Fibonacci sequence counts from 0. If difference is less than 1, the sequence already covers the request and the loop body never runs. If it is 1 or more, the loop appends exactly that many terms by summing the last two elements of the list. The final return slices the list to exactly index elements.
The walrus operator assigns and tests in one expression. difference is both the guard and the loop bound, keeping the extension step minimal.
if (difference := index - (len(self.sequence) - 2)) >= 1:
for _ in range(difference):
self.sequence.append(self.sequence[-1] + self.sequence[-2])
return self.sequence[:index]The main Function: A Persistent REPL Over One Instance
dynamic_programming/fibonacci.py:27main creates one Fibonacci instance and loops forever, so repeat lookups hit the cache rather than recomputing from scratch each time.
The main function creates a single Fibonacci instance on line 34 and enters an interactive loop. This is the key payoff of the class design: the instance persists across every call in the session, so a user who asks for index 100 and then asks again pays the extension cost once and gets the cached result on the second request. A module-level function with a default-argument dict would accomplish the same, but a class makes the cache's lifetime explicit. The loop reads a prompt, accepts exit or quit as sentinels, converts the input to an integer inside a try/except ValueError block per the contribution guide's input-handling requirement, and prints the result. The if __name__ == "__main__" guard on line 50 ensures main() runs only when the file is executed directly.
One persistent instance across the session means the cache grows once. A second request for the same index is O(1) because the sequence is already stored.
def main() -> None:
print(
"Fibonacci Series Using Dynamic Programming\n",
"Enter the index of the Fibonacci number you want to calculate ",
"in the prompt below. (To exit enter exit or Ctrl-C)\n",
sep="",
)
fibonacci = Fibonacci()
while True:
prompt: str = input(">> ")
if prompt in {"exit", "quit"}:
break
try:
index: int = int(prompt)
except ValueError:
print("Enter a number or 'exit'")
continue
print(fibonacci.get(index))Complexity: O(n) Time, O(n) Space, and the Alternatives
dynamic_programming/fibonacci.py:7The class approach is O(n) time and O(n) space. Keeping only two variables drops space to O(1). Binet's formula reaches O(log n) via fast exponentiation.
Appending one element per missing index makes the extension O(n) time. Storing the full sequence makes it O(n) space. For most educational purposes those costs are acceptable, and returning the full prefix list is a natural interface. Reducing space to O(1) requires throwing away all but the last two values, which means a repeat call for any previous index recomputes from scratch. That trade-off matters if memory is tight and repeat calls are rare. The closed-form Binet formula, which uses the golden ratio, gives each term in O(log n) time via fast matrix exponentiation, but introduces floating-point precision issues for large indices. The class-based memoization approach here prioritizes clarity and repeat-call efficiency at the cost of memory proportional to the largest index ever requested. Reading maths/fibonacci.py in this repository shows several of these alternatives side by side.
O(n) time and O(n) space for the class approach. O(1) space is possible by keeping only the last two values; O(log n) time via Binet requires floating-point arithmetic.
class Fibonacci:
def __init__(self) -> None:
self.sequence = [0, 1]
def get(self, index: int) -> list:
"""
Get the Fibonacci number of `index`. If the number does not exist,
calculate all missing numbers leading up to the number of `index`.
>>> Fibonacci().get(10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> Fibonacci().get(5)
[0, 1, 1, 2, 3]
"""
if (difference := index - (len(self.sequence) - 2)) >= 1:
for _ in range(difference):
self.sequence.append(self.sequence[-1] + self.sequence[-2])
return self.sequence[:index]You've walked through 5 key areas of the The Algorithms - Python codebase.
Continue: How the 0/1 Knapsack DP Works → Browse all projectsCreate code tours for your project
Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.
Install Intraview Free