The field guide to infinite patterns

The mathematics of infinity, made visible.

Fractal

Fractal Mathematics

How to Make Fractals in Python

From a fifteen-line Mandelbrot renderer to an animated Barnsley fern, Python makes the infinite tangible. A working guide — with real code, the math behind it, and the libraries that do the heavy lifting.

Diagram illustrating fractal dimensions on a dark background — used as hero for How to Make Fractals in Python
Illustration: Fractal

There is something vertiginous about writing twenty lines of Python and watching your terminal produce a shape with infinite boundary. That is what fractal programming offers: a direct line between elementary arithmetic and one of the most complex geometric objects in mathematics. This guide walks you through four distinct Python techniques — from a minimal Mandelbrot renderer to an animated Barnsley fern — explaining both the code and the mathematics that drives each one.

Key Takeaway: Python's three standard fractal toolkits are (1) NumPy + Matplotlib for escape-time fractals like the Mandelbrot and Julia sets, (2) the turtle module for recursive geometric fractals like the Koch snowflake and fractal trees, and (3) random + matplotlib scatter for iterated function system (IFS) fractals like the Barnsley fern. Each maps cleanly onto a different branch of fractal mathematics.

What libraries do you need to make fractals in Python?

Before writing a single loop, it helps to know which tool fits which fractal. The table below maps each fractal family to its ideal Python stack:

Fractal Type Examples Python Libraries Core Technique
Escape-time Mandelbrot set, Julia sets, Burning Ship NumPy, Matplotlib Iterate z = z² + c per pixel; color by escape speed
Recursive geometric Koch snowflake, Sierpiński triangle, fractal tree turtle (stdlib), Matplotlib Recursive function; subdivide and redraw at each depth
Iterated Function System (IFS) Barnsley fern, Sierpiński gasket, dragon curve random (stdlib), NumPy, Matplotlib Apply probabilistic affine transforms; plot each point
L-systems Fractal plants, Hilbert curve, Lévy curve turtle, string manipulation Rewrite a string by grammar rules; interpret as turtle commands

All four libraries ship with a standard Python 3 installation or are installable in seconds via pip install numpy matplotlib. No exotic dependencies are required.

Can you make the Mandelbrot set in Python?

Yes — and it takes fewer lines than most people expect. The Mandelbrot set is defined by the iteration zn+1 = zn² + c, where both z and c are complex numbers and z starts at zero. For each point c on the complex plane, you iterate the formula repeatedly: if |z| stays bounded (never exceeds 2), the point is in the set and colored black; if it escapes, you color it by how quickly it left. The result is the iconic shape first visualized on an IBM supercomputer in 1980.

The naive pure-Python implementation uses a pixel-by-pixel loop:

import numpy as np
import matplotlib.pyplot as plt

width, height, max_iter = 800, 600, 256
x = np.linspace(-2.5, 1.0, width)
y = np.linspace(-1.25, 1.25, height)
C = x[np.newaxis, :] + 1j * y[:, np.newaxis]  # complex grid

Z = np.zeros_like(C)
M = np.zeros(C.shape, dtype=int)

for i in range(max_iter):
    mask = np.abs(Z) <= 2
    Z[mask] = Z[mask] ** 2 + C[mask]
    M[mask] += 1

plt.figure(figsize=(10, 7))
plt.imshow(M, extent=(-2.5, 1.0, -1.25, 1.25), cmap="inferno", origin="lower")
plt.title("Mandelbrot Set")
plt.colorbar(label="Escape iteration")
plt.tight_layout()
plt.show()

The key insight in this vectorized version — drawn from the authoritative Real Python tutorial and the official NumPy fractal tutorial — is that the mask array prevents us from updating points that have already escaped: once a pixel's z exceeds 2 it is permanently excluded from future iterations. This single boolean mask turns a loop that would otherwise touch 480,000 complex-number multiplications per iteration into a shrinking operation that speeds up automatically as more points escape. At max_iter=256 on modern hardware the full image renders in under two seconds.

Color choice matters more than it might seem. The "inferno" colormap assigns dark purples to points that escape quickly (far outside the set) and bright yellows to points that linger longest at the boundary — visually emphasizing the intricate filaments that define the Mandelbrot set's fractal boundary, whose fractal dimension is exactly 2.

How do you draw a fractal tree and Koch snowflake in Python?

Geometric fractals like the Koch snowflake and fractal trees are built on the mathematical concept of self-similarity: each piece of the structure is a scaled copy of the whole. Python's built-in turtle module — which ships with every standard Python 3 installation — mirrors this structure naturally, because turtle graphics are themselves recursive: to draw a branch, you draw a shorter branch at an angle, then another, then another, until the branches become too small to see.

Here is a classic recursive fractal tree:

import turtle

def draw_branch(t, length, angle=25, scale=0.7):
    if length < 5:
        return
    t.forward(length)
    t.right(angle)
    draw_branch(t, length * scale, angle, scale)
    t.left(angle * 2)
    draw_branch(t, length * scale, angle, scale)
    t.right(angle)
    t.backward(length)

screen = turtle.Screen()
screen.bgcolor("black")
t = turtle.Turtle()
t.color("lime")
t.speed(0)
t.left(90)
t.up()
t.goto(0, -200)
t.down()
draw_branch(t, 120)
turtle.done()

The critical structural element is the pair of recursive calls separated by heading adjustments: t.right(angle) before the first recursive call turns the turtle to draw the right sub-branch; t.left(angle * 2) overshoots back past center to draw the left sub-branch; then t.right(angle) restores the original heading. The t.backward(length) at the base of the function is equally important — it walks the turtle back to the branch's starting point so higher levels of the recursion can continue correctly. Without this backtracking, successive branches would drift off into open space.

For the Koch snowflake, the same recursive logic applies, but the "branching" rule is different: each straight segment is replaced by four segments (the middle third of the original is replaced by two sides of an equilateral triangle):

def koch_curve(t, length, depth):
    if depth == 0:
        t.forward(length)
        return
    length /= 3
    koch_curve(t, length, depth - 1)
    t.left(60)
    koch_curve(t, length, depth - 1)
    t.right(120)
    koch_curve(t, length, depth - 1)
    t.left(60)
    koch_curve(t, length, depth - 1)

A depth of 5 produces a visually rich snowflake; depth 7 or higher starts to reveal the paradox the Koch snowflake illustrates: each iteration multiplies the perimeter by 4/3, so after n iterations the perimeter is (4/3)n times the original — growing without bound even as the enclosed area converges to a finite value. This geometric fact connects directly to the fractal dimension of the Koch curve, which is log(4)/log(3) ≈ 1.262 — strictly between the topological dimension of a line (1) and a filled plane (2).

The official Matplotlib blog demonstrates how to extend this approach to animated fractals, successively revealing each iteration of the Koch snowflake or Mandelbrot set using FuncAnimation.

How do you generate a Barnsley fern in Python?

The Barnsley fern, described by mathematician Michael Barnsley in his 1988 textbook Fractals Everywhere, is a fractal that genuinely looks like a plant — and it is generated by a completely different mechanism than either escape-time or recursive geometric fractals. It uses an Iterated Function System (IFS): four affine transformations are applied randomly with fixed probabilities, each one "pushing" a point into a different region of the final fern shape. Plot enough points (typically 100,000 or more) and the emergent structure is unmistakable.

import random
import matplotlib.pyplot as plt

def barnsley_fern(n=200_000):
    x, y = 0.0, 0.0
    xs, ys = [], []
    for _ in range(n):
        r = random.random()
        if r < 0.01:          # stem
            x, y = 0.0, 0.16 * y
        elif r < 0.86:        # main leaflet
            x, y = 0.85 * x + 0.04 * y, -0.04 * x + 0.85 * y + 1.6
        elif r < 0.93:        # right leaflet
            x, y = 0.20 * x - 0.26 * y,  0.23 * x + 0.22 * y + 1.6
        else:                  # left leaflet
            x, y = -0.15 * x + 0.28 * y, 0.26 * x + 0.24 * y + 0.44
        xs.append(x)
        ys.append(y)
    return xs, ys

xs, ys = barnsley_fern()
plt.figure(figsize=(6, 10), facecolor="black")
plt.scatter(xs, ys, s=0.1, c=ys, cmap="Greens", linewidths=0)
plt.axis("off")
plt.title("Barnsley Fern", color="white")
plt.tight_layout()
plt.show()

The four transformation matrices encode the biological structure of a fern: the first (probability 1%) maps everything to the stem; the second (85%) handles the main compound leaf axis; the third and fourth (7% and 7%) generate the sub-leaflets. The magic of the IFS is that the attractor — the shape the random walk converges toward — is entirely determined by the matrices, not by the starting point. You can begin at any (x, y) and after a few hundred "warm-up" iterations the trajectory locks onto the fern. This is a concrete demonstration of the self-similarity principle: each leaflet is a miniaturized, transformed copy of the whole fern.

Coloring the scatter plot by the y-coordinate (height) using a green colormap produces depth cues that reinforce the biological illusion. At 200,000 points the image is crisp; at 500,000 points individual leaflet veins become visible.

How do you speed up Python fractal rendering with NumPy?

Pure Python loops over pixel grids are slow — a 1,000 × 1,000 Mandelbrot image iterated 256 times requires roughly 256 million arithmetic operations. NumPy's vectorized operations run in compiled C code under the hood, typically achieving 50–200× speedups over pure Python on the same calculation.

The core optimization is replacing per-pixel loops with array-wide operations. Instead of:

# Slow: pure Python loop
for i in range(height):
    for j in range(width):
        result[i, j] = mandelbrot(C[i, j], max_iter)

you operate on the entire 2D complex array at once, using a boolean mask to exclude already-escaped points:

# Fast: NumPy vectorized
Z = np.zeros_like(C)
M = np.zeros(C.shape, dtype=int)
for i in range(max_iter):
    mask = np.abs(Z) <= 2          # points still inside
    Z[mask] = Z[mask]**2 + C[mask]  # iterate only bounded points
    M[mask] += 1                    # count iterations

For even greater performance, the SciPy lecture notes demonstrate how to use Numba's @jit decorator to JIT-compile the inner loop to native machine code, often achieving speeds within 5× of hand-written C. For very high-resolution renders (4K and above), the multiprocessing module can distribute rows of the image across CPU cores, since each row is computed independently.

A practical reference point: on a modern laptop, a 2,000 × 2,000 Mandelbrot at 512 iterations takes roughly 40 seconds in pure Python, 1.5 seconds with vectorized NumPy, and under 0.3 seconds with Numba JIT. For exploratory work, NumPy is almost always sufficient.

What are the best Python packages for fractal visualization beyond Matplotlib?

Matplotlib is the workhorse for static images, but the Python fractal ecosystem extends considerably beyond it:

  • HoloViews + Datashader — handles billions of IFS points without memory exhaustion, ideal for ultra-high-density Barnsley fern or flame fractal renders.
  • Pillow (PIL) — produces direct pixel-level image output without a display window; useful for saving fractal images to disk programmatically. The Real Python Mandelbrot tutorial demonstrates Pillow rendering alongside Matplotlib.
  • Pygame — enables real-time interactive fractal explorers with keyboard zoom controls; the rendering loop maps naturally onto the escape-time algorithm.
  • Manim — the mathematical animation library (behind the 3Blue1Brown YouTube channel) can animate fractal construction step by step, ideal for educational visualizations.
  • mpmath — Python's arbitrary-precision mathematics library, essential when zooming deep into the Mandelbrot set where standard 64-bit floats lose resolution (typically below magnifications of 1015).

For three-dimensional fractals — Mandelbulbs, Menger sponges, Apollonian gaskets — the dedicated software Mandelbulb 3D and Mandelbulber are purpose-built and faster than anything achievable in pure Python; but Python scripts can still drive them programmatically via their command-line interfaces.

If you want to share your fractals as interactive web visualizations, Plotly's imshow function renders escape-time fractals as zoomable, pannable HTML canvases directly exportable to a web page — no server required.

Frequently asked

Can I make fractals in Python as a beginner?

Yes. Python's built-in turtle module requires no additional installations and produces recognizable fractal trees and Koch snowflakes in under 20 lines of recursive code. The base case is simply "stop when the branch is too short" — which maps directly onto how students learn recursion. Once comfortable with turtle, the jump to NumPy-powered Mandelbrot rendering is well-documented; the Real Python tutorial and the official NumPy fractal tutorial both walk through every step with working code.

What is the mathematical formula used for the Mandelbrot set in Python?

The Mandelbrot set is generated by the iteration zn+1 = zn² + c, where z starts at zero and c is the complex-plane coordinate of the pixel being tested. If repeated application of this formula keeps |z| bounded (conventionally below 2), the point c is inside the set and colored black. If |z| escapes past 2, the point is outside the set and colored by the iteration count at which it escaped — producing the characteristic gradient halos. In Python, complex arithmetic is native: z = z**2 + c is all that is needed per iteration.

How do you make the Barnsley fern fractal in Python?

The Barnsley fern uses an Iterated Function System (IFS): four affine transformations with fixed probabilities (1%, 85%, 7%, 7%) are applied in sequence to a single point. Each transformation encodes part of the fern's biology — stem, main leaf axis, right leaflet, left leaflet. After 100,000 or more iterations, plotting all visited points produces the complete fern shape. The key libraries are Python's built-in random module for the probability sampling and Matplotlib's scatter for rendering. Michael Barnsley described the algorithm in his 1988 textbook Fractals Everywhere.

How do you speed up fractal rendering in Python?

The most impactful optimization is switching from pure Python loops to NumPy vectorized operations, which run in compiled C code and typically deliver 50–200× speedups. The key technique is operating on the entire complex-number grid as a single NumPy array and using a boolean mask to skip points that have already escaped. For even faster rendering, the Numba library's @jit decorator JIT-compiles the iteration loop to native machine code; the SciPy lecture notes demonstrate this approach. For high-resolution work, Python's multiprocessing module distributes image rows across CPU cores independently.

What is an L-system and how do you use one in Python?

An L-system (Lindenmayer system) represents a fractal as a string rewriting rule: a starting string is repeatedly transformed by substitution rules, then interpreted as turtle-graphics commands. For the Koch snowflake, the rule is F → F+F-F-F+F where F = move forward, + = turn right 60°, − = turn left 60°. In Python, the string generation is a simple dictionary substitution loop; the interpretation uses the turtle module to draw each character. L-systems can encode complex branching plant architectures, space-filling curves like the Hilbert curve, and the dragon curve — all from a handful of grammar rules.

How deep can you zoom into a Python-rendered fractal before it breaks?

Standard 64-bit floating-point arithmetic (Python's default) begins losing precision at zoom magnifications of roughly 1014 to 1015, where the gaps between representable floating-point numbers become larger than the features being rendered. Beyond that threshold, the image degenerates into square artifacts. To zoom deeper, use Python's mpmath library for arbitrary-precision arithmetic — specifying mp.dps = 50 gives 50 decimal places of precision, enabling zooms past 1040 and beyond, at the cost of significantly slower computation. YouTube deep-zoom videos reaching magnifications of 101077 use highly optimized multi-precision renderers written in C++.

Can Python generate 3D fractals like the Mandelbulb?

Python can generate 3D fractals, but not efficiently in pure Python due to the volume of calculations required. The Mandelbulb extends the Mandelbrot iteration into 3D using a spherical-coordinate power formula; a basic Python/NumPy implementation is feasible for low-resolution previews but will be slow. For production-quality 3D fractal renders, dedicated tools like Mandelbulb 3D or Mandelbulber are purpose-built and can be driven from Python via their command-line interfaces. For 3D IFS fractals like the Menger sponge, constructed by iteratively removing cubes, Python with Matplotlib's 3D axes or the Mayavi library provides workable visualizations.