Deep Inheritance and Design Complexity: A Technical Review of Manim
Overview: The Architecture of Mathematical Animation
Designing a system that translates abstract mathematical concepts into fluid, high-fidelity animations is a monumental task.
At its core, the library must manage a "scene graph"—a collection of objects, their properties (like color, stroke, and position), and the transformations they undergo over time. While the visual output is undeniably stunning, the underlying code architecture reveals the challenges of scaling a project from a personal tool to a widely used open-source framework. This tutorial explores the technical design of Manim, focusing on its inheritance structures, namespace management, and the trade-offs between deep object-oriented hierarchies and functional composition.
Prerequisites: Foundations for Framework Analysis
To get the most out of this walkthrough, you should have a firm grasp of Python 3.7+. Specifically, you need to understand how Object-Oriented Programming (OOP) works in Python, including class inheritance, the super() function, and the difference between instance and class variables. Familiarity with Scene Graphs—the data structures used to represent 2D or 3D scenes—will help you understand why the library is structured as a tree of objects. Experience with external rendering tools like

Key Libraries & Tools
- Manim: The primary animation engine for explanatory math videos.
- SciPy: Used within the library for complex mathematical calculations, such as Bézier curve interpolation.
- FFmpeg: The external command-line tool Manim uses to render and encode video files.
- Pathlib: A recommended standard library for modern Python projects to handle filesystem paths cleanly.
Code Walkthrough: Analyzing the Inheritance Stack
One of the most striking aspects of Manim is its reliance on deep inheritance. Let's look at how a simple Text object is constructed by tracing its ancestry through the codebase. This reveals a hierarchy that is often six or seven levels deep.
1. The Entry Point: Global Namespace Issues
Manim often uses a style of importing that is generally discouraged in professional software development: the wildcard import.
from manimlib import *
By importing everything, the global namespace becomes polluted. You lose track of where constants like UP, DOWN, or specific primitives originate. This makes the code harder to lint and maintain because the source of truth is buried inside the package's __init__.py files.
2. The Ancestry of a Text Object
When you create a Text object in Manim, you aren't just creating a string renderer. You are instantiating a complex chain of classes.
class Text(MarkupText):
# ... methods for text handling
class MarkupText(StringMobject):
# ... methods for parsing tags
class StringMobject(SVGMobject):
# ... abstract base class for string-based SVGs
class SVGMobject(VMobject):
# ... logic for parsing SVG paths and shapes
class VMobject(Mobject):
# ... Vectorized Mathematical Object logic
class Mobject(object):
# ... The root Mathematical Object
This structure means that the Text class inherits over a hundred methods and properties. While this allows for powerful abstractions, it creates tight coupling. A change in the VMobject base class ripples down to every single sub-component in the library, making refactoring a high-risk activity.
3. The Brittle Nature of Initializers
In Manim, each class in the hierarchy has its own __init__ method with dozens of arguments. The order in which super().__init__(**kwargs) is called varies significantly between classes.
class VMobject(Mobject):
def __init__(self, **kwargs):
# Logic happens first
self.stroke_width = kwargs.get("stroke_width", 4)
# Then the super call
super().__init__(**kwargs)
class SVGMobject(VMobject):
def __init__(self, file_name=None, **kwargs):
# Super call happens in the middle of initialization
super().__init__(**kwargs)
self.file_name = file_name
self.init_colors()
When super() calls are scattered inconsistently—sometimes at the start, sometimes at the end, and sometimes in the middle—it becomes nearly impossible to track the state of an object during construction. This is a "brittle" design pattern that can lead to unexpected bugs where attributes are overwritten or accessed before they are properly defined.
Syntax Notes: Class vs. Instance Variables
Manim frequently mixes class-level variables and instance-level variables in ways that can be confusing for learners.
class StringMobject(SVGMobject):
height = 2.0 # Class variable: Shared across all instances
def __init__(self, **kwargs):
self.content = "" # Instance variable: Unique to this object
super().__init__(**kwargs)
Using class variables for defaults like height is a common pattern, but it requires discipline. If a developer accidentally modifies StringMobject.height, every subsequent string object created will use that new default. It is often safer to use a Data Class approach or to define these defaults within the __init__ method to ensure instance isolation.
Practical Examples: Moving Toward Composition
If we were to refactor Manim, we would look toward Composition over Inheritance. Instead of a Text object being an SVGMobject, a Text object could have a renderer and have a set of path data.
Consider this alternative structure:
class SceneNode:
def __init__(self):
self.data = {} # Pure data focus
self.modifiers = [] # Behaviors as pluggable functions
def apply_stroke(node, width):
node.data['stroke_width'] = width
return node
By moving behavior out of the classes and into standalone functions, the system becomes much easier to test. You no longer need to instantiate a massive 7-level object hierarchy just to test if a color conversion function works correctly.
Tips & Gotchas: Best Practices for Large Frameworks
- Avoid Wildcard Imports: Always use explicit imports (e.g.,
from manimlib.mobject.types.vectorized_mobject import VMobject). It makes your dependencies clear to other developers and your IDE. - Keep Inheritance Shallow: Aim for no more than 3 levels of inheritance. If you find yourself going deeper, consider if the relationship is truly an "is-a" relationship or if it should be a "has-a" relationship (composition).
- Rely on Established Libraries: Manim implements its own Bézier curve logic. While impressive, using specialized libraries like SciPyor a dedicated curve-handling package reduces the maintenance burden and likely improves performance through C-extensions.
- Testable Utilities: Large frameworks should keep their
utilsmodules pure. Functions likeclip(value, min_val, max_val)should be simple, stateless, and have 100% test coverage. In Manim, some utility functions likefind_fileactually perform side effects like downloading files from the internet—this is a design "gotcha" that can surprise users with unexpected network activity.

Fancy watching it?
Watch the full video and context