Python Nested Classes A Practical Guide for AI Devs
You’re probably looking at a Python file that started simple and now feels crowded.
Maybe you built an LLMClient class, then added a config object, a custom response parser, a couple of internal state types, and one odd little helper class that only matters to that client. Suddenly the module reads like a junk drawer. Everything works, but the code doesn’t feel tidy.
That’s where python nested classes can help. They’re not a feature you reach for every day, and that’s exactly why they confuse people. Used well, they keep tightly related code together. Used badly, they make simple code look clever and harder to debug. The trick is knowing the difference.
What Are Python Nested Classes Anyway
A nested class is just a class defined inside another class. That’s it.
The reason this matters isn’t syntax. It’s organization. When a helper class only makes sense inside one parent class, nesting lets you keep that helper close to the code that uses it. You’re saying, “this type belongs here.”

If you’re still getting comfortable with classes in general, a beginner-friendly Python programming PDF guide can help before you lean into more advanced object-oriented patterns like this.
Why developers use them
Say you’re building an AI model wrapper. The main class might need:
- A config type for model settings
- A validator for request payloads
- A custom error class used only by that wrapper
- A tiny internal state object for tracking retries or token usage
You could define all of those at module level. But if none of them should be used outside the wrapper, nesting can make the code easier to scan.
Practical rule: If a class has no meaningful life outside one parent class, nesting is worth considering.
What nested classes are not
They are not magical child objects that automatically inherit everything from the outer class.
That’s the first place people trip up. A nested class lives inside the outer class’s namespace, but an instance of the inner class doesn’t automatically get the outer instance’s self. Python keeps those ideas separate on purpose.
That separation is a good thing. It keeps relationships explicit instead of hidden.
Understanding the Class Inside a Class Structure
The easiest way to think about python nested classes is a toolbox.
You have one big toolbox. Inside it, you keep specialized compartments. Those compartments exist to support the toolbox’s purpose. You don’t carry around a “screw compartment” as a separate object in daily life. It belongs inside the toolbox.
That’s what a nested class does in code.

Python has supported this pattern since Python 1.0 in 1994, and nested classes have remained part of the language for over 30 years. They still show up in advanced object-oriented patterns used by major AI frameworks like TensorFlow and PyTorch, which together have over 250,000 GitHub stars as of early 2026 according to this overview of Python inner classes.
The smallest example
Here’s the basic structure:
class ModelService:
class Config:
def __init__(self, model_name, temperature):
self.model_name = model_name
self.temperature = temperature
You access the nested class through the outer class name:
cfg = ModelService.Config("gpt-style-model", 0.2)
print(cfg.model_name)
That can feel slightly unusual at first, but it’s straightforward once you read it as, “Config belongs to ModelService.”
Creating an inner object from the outer class
Now let’s make the outer class use its nested class.
class ModelService:
class Config:
def __init__(self, model_name, temperature):
self.model_name = model_name
self.temperature = temperature
def __init__(self):
self.config = self.Config("embedder-v1", 0.0)
Now:
service = ModelService()
print(service.config.model_name)
This is often the first “aha” moment. The nested class isn’t a different kind of class. It’s still a normal class. The only difference is where you defined it and how you reference it.
Why this helps readability
When someone opens your file and sees this pattern, they learn something immediately:
- The nested type is related to the parent
- It probably isn’t intended for broad reuse
- The author wants to keep the module namespace cleaner
That third point matters more than people think. In a large file, top-level names add up fast.
Here’s the contrast.
Without nesting:
class ModelConfig:
pass
class ModelStatus:
pass
class ModelValidationError(Exception):
pass
class ModelService:
pass
With nesting:
class ModelService:
class Config:
pass
class Status:
pass
class ValidationError(Exception):
pass
Both versions are valid. The second version tells a clearer story when those helper types are tightly coupled to ModelService.
You’re not making the code more powerful. You’re making the relationship between names more obvious.
A detail worth remembering
A nested class can be instantiated directly from the outer class, even if you never create an outer instance.
cfg = ModelService.Config("classifier-v2", 0.1)
That surprises beginners because “inside another class” sounds like the outer object must exist first. It doesn’t. The nested class is attached to the outer class, not stored inside each outer instance by default.
Navigating Scope Between Inner and Outer Classes
This is the part that usually causes bugs.
People see a class inside another class and assume the inner one can naturally read the outer object’s attributes. It can’t. A nested class definition sits in the outer class’s namespace, but instances of that nested class do not automatically receive the outer instance.
A clean mental model is this:
- The outer class body is where Python stores names like
ConfigorCPU - The outer instance is the object created from that class
- The inner instance is a separate object unless you explicitly connect them
Outer class accessing the inner class
This direction is simple. The outer class can create an instance of its nested class using self.InnerClassName().
A common example is a Computer class with a nested CPU class:
class Computer:
class CPU:
def process(self):
return "Processing instructions"
def __init__(self):
self.cpu = self.CPU()
That pattern is a standard use case for encapsulation. In practical guidance on inner classes, a Computer class can instantiate a nested CPU with self.cpu = self.CPU(), and the same guidance notes that using explicit references for outer state can reduce naming conflicts by 20 to 30% in large projects in modular designs, as discussed in this DataCamp tutorial on inner classes.
Now usage is easy:
pc = Computer()
print(pc.cpu.process())
Why inner classes can’t just read outer self
This will not work the way people expect:
class Computer:
def __init__(self, hardware_version):
self.hardware_version = hardware_version
class CPU:
def show_version(self):
return self.hardware_version
The self inside CPU.show_version refers to a CPU instance, not a Computer instance.
That’s the key distinction. The inner class doesn’t get a secret tunnel to the outer object.
The correct pattern
If the inner object needs access to outer state, pass the outer instance in explicitly.
class Computer:
def __init__(self, hardware_version):
self.hardware_version = hardware_version
self.cpu = self.CPU(self)
class CPU:
def __init__(self, outer):
self.outer = outer
def show_version(self):
return self.outer.hardware_version
Usage:
pc = Computer(hardware_version=3)
print(pc.cpu.show_version())
This is explicit, readable, and testable.
Hidden coupling creates confusing bugs. Explicit references create understandable code.
A real AI-flavored example
Suppose you’re building a model-serving class and the nested validator needs the outer config.
class ModelServer:
def __init__(self, max_tokens):
self.max_tokens = max_tokens
self.validator = self.RequestValidator(self)
class RequestValidator:
def __init__(self, server):
self.server = server
def validate(self, payload):
tokens = payload.get("tokens", 0)
return tokens <= self.server.max_tokens
Now the validator can read the server’s current setting without pretending it owns that state.
server = ModelServer(max_tokens=2048)
print(server.validator.validate({"tokens": 512})) # True
print(server.validator.validate({"tokens": 5000})) # False
Class-level access is different from instance-level access
There’s another source of confusion. A nested class can access names on the outer class if you refer to the outer class explicitly.
class Outer:
version = 2
class Inner:
def get_version(self):
return Outer.version
That works because Outer.version is a class attribute. It’s not the same as reading outer_instance.version.
Here’s the practical difference:
| Access target | How you reach it | Example |
|---|---|---|
| Outer class attribute | Reference outer class by name | Outer.version |
| Outer instance attribute | Pass outer instance explicitly | self.outer.version |
| Inner class from outer instance | Use self.InnerClass |
self.CPU() |
The mistake I see most often
Developers write a nested class because they want stronger conceptual grouping, which is fine. Then they start treating it like a closure, expecting it to “capture” the outer object.
Classes don’t work like nested functions. A nested function can close over variables from an enclosing scope. A nested class doesn’t automatically do that with instance state.
When you remember that, most confusion disappears.
A simple rule of thumb
Use this checklist whenever an inner class needs data from the outer one:
- If the data is fixed for all instances, a class attribute may be enough.
- If the data belongs to one specific outer object, pass that outer object in.
- If the inner class needs a lot of outer state, pause and reconsider the design.
That last one matters. If your inner class needs half the parent object to function, composition or a separate class may be cleaner.
Practical Python Nested Class Examples
The best way to decide whether python nested classes are useful is to see them in code that feels real.
In AI and ML work, this pattern comes up when you need a type that’s tightly bound to one parent object. According to the discussion around nested definitions and usage patterns, 49.3% of Python developers using Python for machine learning reported using advanced OOP including nesting in Stack Overflow’s 2024 survey. That same discussion points to libraries like Hugging Face Transformers using inner classes to manage tokenizers, and notes the library has over 250,000 GitHub stars.

A model class with a nested config
This is probably the most practical pattern for beginners.
from dataclasses import dataclass
class EmbeddingModel:
@dataclass
class Config:
model_name: str
batch_size: int
normalize: bool = True
def __init__(self, config):
self.config = config
def embed(self, texts):
return {
"model": self.config.model_name,
"count": len(texts),
"normalized": self.config.normalize
}
Usage:
config = EmbeddingModel.Config(
model_name="sentence-encoder",
batch_size=16,
normalize=True
)
model = EmbeddingModel(config)
print(model.embed(["hello", "world"]))
Why nest Config here?
Because EmbeddingModel.Config reads naturally. The config belongs to that model. If your codebase has lots of configs, top-level names like Config, ModelConfig, EmbeddingConfig, TrainingConfig, and InferenceConfig can get noisy fast.
This pattern also helps teammates discover the API. When they inspect EmbeddingModel, they immediately see that there’s a dedicated config type attached to it.
A request class with a nested enum-like state container
You don’t always need a full standalone class for internal states.
from enum import Enum
class InferenceRequest:
class Status(Enum):
PENDING = "pending"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
def __init__(self, prompt):
self.prompt = prompt
self.status = self.Status.PENDING
def start(self):
self.status = self.Status.RUNNING
def finish(self):
self.status = self.Status.DONE
Usage:
req = InferenceRequest("Summarize this report")
print(req.status.value)
req.start()
print(req.status.value)
This works well when the status values only matter in the context of InferenceRequest. If the same status enum will be shared across task queues, APIs, and UI serializers, I’d pull it out into its own top-level type.
That’s the bigger lesson. Nesting is strongest when the inner type is local in meaning, not broadly reusable.
A tiny neural network sketch with a nested helper
Here’s a lightweight example that feels closer to ML code.
class Layer:
def __init__(self, weights, bias):
self.weights = weights
self.bias = bias
self.neurons = [self.Neuron(self, i) for i in range(len(weights))]
class Neuron:
def __init__(self, layer, index):
self.layer = layer
self.index = index
def activate(self, inputs):
weight_row = self.layer.weights[self.index]
total = sum(w * x for w, x in zip(weight_row, inputs)) + self.layer.bias
return max(0, total) # ReLU
Usage:
layer = Layer(
weights=[
[0.2, 0.8],
[0.5, -0.1]
],
bias=0.1
)
output_1 = layer.neurons[0].activate([1.0, 2.0])
output_2 = layer.neurons[1].activate([1.0, 2.0])
print(output_1, output_2)
This is a nice teaching example because it shows the nested helper needing an explicit reference to the outer Layer.
Would I always ship code like this in production? Not necessarily. If Neuron grows into a substantial type with its own tests, serialization needs, and reuse, I’d likely move it out. But for a tightly coupled helper, nesting keeps the mental model neat.
A custom exception that belongs to one class
This is another underrated use case.
class VectorStore:
class QueryError(Exception):
pass
def query(self, text):
if not text:
raise self.QueryError("Query text cannot be empty")
return ["doc1", "doc2"]
Usage:
store = VectorStore()
try:
store.query("")
except VectorStore.QueryError as exc:
print(f"Handled store-specific error: {exc}")
This is clean because the exception name is scoped. You don’t end up with many free-floating exceptions like QueryError, ParseError, or ConfigError at module level with unclear ownership.
For related graph-style data work, this depth first search in Python guide is also useful if your AI application traverses tree or graph structures and you’re thinking carefully about code organization.
A quick walkthrough of a common inheritance pitfall
Nested classes get trickier once inheritance enters the picture. This short video covers one of the most common mistakes developers make with nested class inheritance:
The trap usually looks simple at first. You try to make an inner class inherit from the outer class while the outer class is still being defined. Python can’t resolve that relationship yet.
My practical opinion on these examples
If you’re new to object-oriented Python, don’t try to nest everything. Start with one of these patterns:
- Config type when the settings belong to one class
- Exception type when the error is class-specific
- Status enum when state names are tightly coupled
- Internal helper object when it shouldn’t exist anywhere else
That’s enough to build intuition. Once you’ve written and tested a few, you’ll feel when the pattern helps and when it’s just decoration.
When to Use Nested Classes in Your Projects
At this point, judgment matters more than syntax.
Nested classes are useful, but they’re not automatically “better OOP.” In everyday Python, they’re still relatively uncommon. That makes them a deliberate design choice, not a default habit. When you choose them, there should be a clear reason.
Good reasons to use them
A nested class earns its place when the relationship is tight and obvious.
Class-specific config objects
Trainer.Configis easier to understand than a genericConfigsitting elsewhere in the file.Private-ish helper types
If a parser, validator, or state object only serves one parent class, nesting signals that intent.Scoped exceptions
DatasetLoader.ValidationErroris more descriptive than another top-levelValidationError.Small internal models in AI workflows
A serving class might keep a nested request state or response schema helper that nobody else should instantiate directly.
If moving the inner class to the top level makes the code feel more confusing, nesting is probably helping.
When nesting makes code worse
The anti-pattern is easy to spot once you’ve seen it a few times.
Sometimes developers nest a class because it feels advanced. The result is a maze of objects with weak boundaries and too many dependencies on the parent.
Avoid nesting when:
- The inner class is reused in multiple places
- The inner class is large enough to deserve its own file or module
- It needs a lot of outer instance state to work
- A function or dataclass would provide a more straightforward solution to the problem
- You’re adding nesting only to hide complexity instead of reducing it
A practical comparison
Here’s a decision table I’d use with a junior developer on my team.
| Scenario | Best Approach | Why |
|---|---|---|
| A config object only makes sense for one model wrapper | Nested class | The relationship is clear and the namespace stays clean |
| A validator is reused by several services | Standalone class | Reuse matters more than tight grouping |
| A simple formatting helper has no state | Function | A class would be heavier than needed |
| A child object needs rich behavior but should be swappable | Composition with separate class | Independent testing and replacement are easier |
| A class-specific exception is only raised by one parent | Nested class | Ownership is obvious to readers |
| A helper type is growing fast and getting shared | Separate module-level class | Nesting starts hiding useful structure |
The inheritance trap people hit
One of the least understood parts of python nested classes is inheritance.
Trying this will fail:
class Outer:
class Inner(Outer):
pass
It fails because Outer isn’t fully defined yet. Python is still executing the class body, so the outer class name is not ready for that inner inheritance relationship. That specific pitfall is a common source of confusion, and it’s called out in this video discussion on nested class inheritance pitfalls.
A safer pattern is to inherit from some other already-defined class:
class BaseValidator:
def validate(self, value):
return value is not None
class APIClient:
class RequestValidator(BaseValidator):
def validate(self, value):
if not super().validate(value):
return False
return isinstance(value, dict)
That’s valid because BaseValidator already exists.
What about inherited outer classes
Another subtle point. If you inherit the outer class, the nested class is still reachable through the subclass unless you replace it.
class BasePipeline:
class Config:
mode = "base"
class TrainingPipeline(BasePipeline):
pass
print(TrainingPipeline.Config.mode)
That can be handy, but it can also surprise readers. If your design relies heavily on subclassing nested classes through inherited outers, slow down and make sure the indirection is worth it.
My rule set for real projects
When I mentor junior Python developers, I usually give them this filter:
- Use nested classes for ownership clarity
- Avoid them for cleverness
- Prefer top-level classes for reuse
- Prefer functions for tiny stateless helpers
- Prefer composition when the child object is a meaningful collaborator
That’s not dogma. It’s a bias toward readable code.
Python lets you nest classes because sometimes the relationship is tighter than a file-level definition can express. But Python also rewards simplicity. If nesting makes the code harder to explain in one sentence, it probably isn’t helping.
Advanced Considerations for Production Code
Once nested classes leave tutorial land and enter real services, three concerns come up quickly: performance, serialization, and testing.
Performance is usually not the issue
For most production applications, nested classes don’t add enough overhead to drive architecture decisions. Guidance around nested definitions discusses the cost as negligible in normal use, so performance usually isn’t the reason to reject the pattern.
That means your decision should rest on readability and maintainability first.
Don’t optimize away a clear design because of tiny overhead that your application will never notice.
Serialization can get awkward
If you’re saving objects with pickle or moving them across process boundaries, nested classes deserve extra care.
The short version is simple:
- Top-level classes are easier for serializers to resolve
- Nested classes can work, but you should test the exact workflow you plan to deploy
- Refactors can hurt deserialization if you move or rename nested types later
If a nested class instance needs to survive long-term persistence, job queues, or worker handoff, I’d validate that behavior early. Don’t assume “it worked in a notebook” means it’s safe in production.
Testing them directly
A hidden class is still just a class. You can test it directly by referencing it through the outer class.
class FeatureStore:
class KeyBuilder:
def build(self, namespace, item_id):
return f"{namespace}:{item_id}"
A test can target it cleanly:
def test_key_builder():
builder = FeatureStore.KeyBuilder()
assert builder.build("user", 42) == "user:42"
If the nested class needs the outer instance, create that dependency explicitly in the test.
class Service:
def __init__(self, prefix):
self.prefix = prefix
class Formatter:
def __init__(self, outer):
self.outer = outer
def format(self, value):
return f"{self.outer.prefix}-{value}"
Test:
def test_formatter():
service = Service("run")
formatter = Service.Formatter(service)
assert formatter.format("123") == "run-123"
Documentation matters more than usual
Because nested classes are less common, they benefit from stronger naming and docstrings. If another developer sees Pipeline.State, they should understand what it represents without digging through five methods.
If you want a solid checklist for naming, docstrings, and maintainable API descriptions, this guide to Python documentation best practices is worth reading.
A practical deployment mindset
For containerized apps, I also like to verify serialization and import behavior inside the same environment I’ll ship. A local script may behave one way, while a packaged service behaves slightly differently. If you’re packaging Python services for deployment, a refresher on how to create a Docker container helps when you want repeatable test environments.
My own production bias is simple:
- Keep nested classes small
- Test them directly
- Be cautious with long-term serialization
- Document intent clearly
If you follow those four rules, nested classes can stay elegant instead of becoming mysterious.
Frequently Asked Questions About Nested Classes
Can a nested class inherit from another class
Yes. It can inherit from any class that’s already defined and available in scope.
class BaseTokenizer:
def tokenize(self, text):
return text.split()
class Pipeline:
class Tokenizer(BaseTokenizer):
pass
What usually fails is trying to inherit from the enclosing outer class during its own definition.
Are nested classes considered Pythonic
Sometimes. It depends on whether they make the code easier to read.
If the nested type clearly belongs to one parent and isn’t broadly reusable, many Python developers will see it as a reasonable design choice. If it adds ceremony without improving clarity, it won’t feel Pythonic.
How do decorators work with nested classes
They work the same way they do with top-level classes.
from dataclasses import dataclass
class Trainer:
@dataclass
class Config:
epochs: int
learning_rate: float
The decorator applies to the nested class normally. The only real difference is how you access the result, which would be Trainer.Config.
If you're exploring Python, AI development, or practical implementation guides for real projects, YourAI2Day is a useful place to keep learning. It’s especially helpful if you want approachable explanations that connect core programming ideas to modern AI tools and workflows.
