Beginner’s Guide to Abstract Classes in Python

Travis Cuzick
Travis Cuzick
hero image

So you’ve learned how to write classes in Python, and maybe you’ve even used inheritance. But then you come across something called an “abstract class” and it sounds... weird. 

Why would you ever write a class you can’t use?

Honestly? Because they solve problems that you might not even know you have! In this guide I’ll walk through what they are, how they work in Python, and why they’re worth knowing.

Sidenote: If you find any of this confusing, or simply want a deep dive into Python, check out Andrei's Python Coding course taken by 200,000+ people:

It’ll take you from an absolute beginner to understanding everything you need to be hired ASAP.

Alternatively, if you're already pretty good at Python and want to build some interesting and useful projects, why not check out my course on Python Automation:

It'll show you how to automate all of the boring or repetitive tasks in your life - and build some pretty standout portfolio projects as well!

With all that out of the way, let's get into this 5-minute tutorial!

What is an abstract class and why does it matter?

Let’s say you’re building a program that deals with payments, and you know you’ll need to support credit cards, PayPal, and maybe even crypto one day. 

But no matter what the actual method is, every payment type should have the same basic functions like authorize() and process().

So, you start with a base class called Payment, and then create subclasses like CreditCardPayment and PayPalPayment

Seems simple but here’s the problem: What if someone accidentally creates an instance of Payment directly? Or, forgets to define authorize() in one of the subclasses? 

Your program might break in weird ways you won’t realize until much later, which is not great when your program is made for processing payments! 

The good news is that this is exactly the kind of mess abstract classes are built to prevent.

How abstract classes work

An abstract class is like a blueprint that says: “Every class that inherits from me must implement these specific methods”. 

This means that it can’t be used on its own; rather, it must only be inherited from. So if you try to skip a required method in a class that inherits from an abstract class, Python will raise an error before your code even runs.

Handy right?

This gives you a safe starting point when you're designing systems that share structure but differ in details, because it forces consistency across related classes without making assumptions about the specifics.

So now that you understand how abstract classes work and why you would want them in your code, let's walk through how to use them.

How to use an abstract class in Python (step by step)

For the sake of simplicity, let’s continue with our example of building a program that handles different types of payments.

First off, you need to define a base class. 

For this example you’ll use the abc module and mark any methods you want to require with @abstractmethod:

from abc import ABC, abstractmethod

class Payment(ABC):

    @abstractmethod
    def authorize(self):
        pass

    @abstractmethod
    def process(self):
        pass

At this point, you’ve created a blueprint. You’re basically saying: “Any class that inherits from this must include BOTH of these methods.” 

But because it’s just a blueprint, Python won’t let you create a Payment() object directly, so now let’s create a real payment class that actually works:

class CreditCardPayment(Payment):

    def authorize(self):
        print("Authorizing credit card...")

    def process(self):
        print("Processing credit card payment...")

Simple! 

Now you can use it like any normal class:

payment = CreditCardPayment()
payment.authorize()
payment.process()

As you can see, everything works, because you followed the structure of the abstract class. But watch what happens when the rules aren’t followed, and someone forgets to implement one of the required methods: 

class IncompletePayment(Payment):

    def authorize(self):
        print("Only this one is defined")

# Raises:
# TypeError: Can't instantiate abstract class IncompletePayment with abstract method process
Python catches it immediately!

This kind of early feedback is incredibly helpful, especially as your codebase grows. Without abstract classes, this sort of mistake could slip by unnoticed until much later, or when someone runs the wrong subclass and hits a weird bug during processing.

But it gets even better, because abstract classes don’t just enforce structure - they also let you define shared behavior. 

For example

Let’s say you want every payment type to send a receipt after it’s processed, but you don’t want to rewrite that logic in every subclass. 

So instead, you just include it in the base class:

class Payment(ABC):

    @abstractmethod
    def authorize(self):
        pass

    @abstractmethod
    def process(self):
        pass

    def receipt(self):
        print("Sending email receipt...")

Now every payment type - from credit card, PayPal, to even crypto - can inherit this method automatically without touching it.

And you’re not limited to just one subclass either. 

For example

Let’s say you want to add another payment option:

class PayPalPayment(Payment):

    def authorize(self):
        print("Authorizing PayPal account...")

    def process(self):
        print("Processing PayPal payment...")

As long as the structure is followed, Python stays happy, and your logic remains clean and predictable - no matter how many new types you add.

That’s the real power of abstract classes:

  • You make sure every subclass does what it’s supposed to do

  • You avoid repeating code across similar classes

  • And you catch problems before they turn into bugs

Now sure, when you’re working on a small script this all might feel like overkill, but in a real project with different developers working on different parts of the system, abstract classes help everyone stay aligned. They set expectations, reduce ambiguity, and give you clean, reliable foundations to build on.

That’s why abstract classes aren’t just a Python feature; they’re a design tool. And once you get used to them, you’ll wonder how you managed larger projects without them.

Common mistakes when using abstract classes in Python

Abstract classes are incredibly helpful. However, as with any tool, once you play around with them a little you’re bound to have a few issues.

So now let’s look at the most common ones you’re likely to run into, and how to avoid them.

Mistake #1: Thinking your subclass is complete when it’s not

One of the easiest and most common mistakes is having everything set up structurally, only for a typo or similar issue to throw a wrench in your code.

Maybe you renamed a method in the base class and forgot to update it in the subclass. Or maybe you left one out entirely without realizing it.

The solution is simple

Before trying anything else, double check that every method marked with @abstractmethod in the base class is also present in the subclass, with the exact same name and parameters. Even one missing argument can cause this error.

Mistake #2: Overusing abstract methods when you don’t need to

Once you understand how abstract methods work, it’s tempting to overuse them. You start marking every method in your base class with @abstractmethod, thinking that the more rules you enforce, the safer your design will be.

But in practice, this can backfire.

Abstract methods are great for enforcing structure, but not every method needs to be implemented by every subclass. Sometimes, a default implementation in the base class is good enough. And if all your subclasses end up writing the exact same method anyway, then it probably shouldn’t be an abstract method!

For example

Let’s say you want every payment type to send a receipt. 

However, if you mark receipt() as abstract like so:

class Payment(ABC):

    @abstractmethod
    def receipt(self):
        pass
Then every subclass has to define it, even if they all implement it the same way!

So now every subclass needs to write this:

def receipt(self):
    print("Sending email receipt...")

This just clutters your codebase while adding no real value, so a better approach would be to simply define receipt() in the base class as a regular method:

class Payment(ABC):

    def receipt(self):
        print("Sending email receipt...")

Now your subclasses get that behavior for free, and devs can override it if they need to. But they’re not forced to duplicate code just to satisfy a requirement that serves no constructive purpose.

This is especially important when your base class grows, because you’ll want to avoid a situation where every subclass has to reimplement five or six methods just because they were marked abstract, even if those implementations are identical.

Tl;DR

Only mark a method as abstract when you know it will need a different implementation in every subclass. If there's a reasonable default that works for most cases, define it in the base class and let subclasses override it only if needed.

Mistake #3: Assuming abstract classes enforce behavior at runtime

This one’s a bit sneaky because it’s true that Python does help you catch missing methods, but only at the point of instantiation. So once your object is created, Python doesn’t care how those methods are used.

Abstract classes only enforce structure, not behavior. Their job is to say: “You must define this method.” They don’t say: “You must use it correctly” which means it can look like it works, but it doesn't.

For example

You might think that defining an abstract method like process() means it has to be called in a certain way, or that it somehow guarantees your logic will be followed, but it doesn’t.

Here’s what that looks like in code:

class Payment(ABC):

    @abstractmethod
    def process(self):
        pass

class CryptoPayment(Payment):

    def process(self):
        # Oops... totally empty
        pass

That technically satisfies Python’s rules because you’ve implemented process(), so no error is raised. But the method doesn’t actually do anything! And when you call it, it will do nothing

TL;DR

Think of abstract classes as a tool for enforcing structure, not logic. They help you say “Every subclass must have this method,” but it’s still your job to test and verify that the method works as expected.

If you want to enforce behavior and not just method presence, then you’ll need to add tests, raise custom exceptions, or combine abstract classes with mixins or other design patterns.

It’s easy to assume abstract classes give you more guardrails than they actually do, but they only catch what’s missing, and not what’s wrong.

Mistake #4: Getting decorator order wrong with @abstractmethod @abstractmethod

If you're combining @abstractmethod with something like @classmethod or @staticmethod, the order you use actually matters. Most people don’t realize this however, often leading to issues later on.

The rule is simple: @abstractmethod must always be

For example

If you want an abstract class method, the correct order looks like this:

class Payment(ABC):

    @classmethod
    @abstractmethod
    def process(cls):
        pass
But if you reverse the order like this, your code won’t behave as you expect:

class Payment(ABC):

    @abstractmethod
    @classmethod
    def process(cls):
        pass
Python still sees ``process()`` as a regular method and not a class method, and this code can break in strange ways as a result. Sometimes you won't get an error until much later, and it won't be obvious what caused it.

This also applies to @staticmethod, so you always want:

@staticmethod
@abstractmethod
def do_something():
    pass
And not the other way around.

TL;DR

If you're using @abstractmethod with other decorators, always place it after them -  otherwise, your method won’t behave the way you intended.

Mistake #5: Using abstract classes when duck typing would be cleaner

Not every problem needs an abstract class.

Sometimes you’re better off writing flexible code that just expects an object to behave a certain way, rather than forcing everything into a rigid hierarchy. That’s where duck typing comes in.

Erm, what?

So in Python, the philosophy is: “If it looks like a duck and quacks like a duck, then it must be a duck”.

All this means is that if an object has the methods you need, it doesn’t matter what its actual type is; you can just call those methods and trust Python to handle it. So if all your code needs is for an object to have a process() method, you don’t necessarily need a base class at all. 

You could instead write something like this:

def handle_payment(payment_object):
    payment_object.process()
As long as the object passed in has a ``process()`` method, this works. You avoid boilerplate, you avoid inheritance, and your code stays simpler.

Using an abstract class here might just add unnecessary structure,  especially in small scripts or one-off utilities where you’re unlikely to scale or reuse components.

TL;DR

Abstract classes are helpful when you’re managing lots of related classes that share structure but differ in implementation. But if all you need is “an object with a method,” duck typing is faster, lighter, and often easier to work with. Don’t add structure unless you need it.

Try out abstract classes in your own code!

Although abstract classes might seem unnecessary at first, they're one of those tools that make more sense the deeper you get into Python. They help you design smarter, cleaner code that scales with less stress. 

As with most anything, the best way to understand abstract classes is to try them out, so go ahead and give them a go in your own code. Maybe build a base class for different kinds of users, messages, or files in a project you’re already working on. 

You don’t need a big system in order to make use of abstract classes - just a program where shared structure can help. And once you see how they work in your own code, all of this stuff is guaranteed to click!

P.S.

Don’t forget, if you want to dive deep into Python then be sure to check out Andrei's Complete Python Developer course:

It’ll take you from an absolute beginner and teach you everything you need to get hired ASAP and [ace the tech interview](/blog/potential-python-interview-questions/).

This is the only Python course you need if you want to go from complete Python beginner to getting hired as a Python Developer this year!

Alternatively, if you're already pretty good at Python and want to build some interesting and useful projects, why not check out my course on Python Automation:

It'll show you how to automate all of the boring or repetitive tasks in your life - and makes for some pretty stand out portfolio projects!

Plus, as part of your membership, you'll get access to both of these courses and others, and be able to join me and 1,000s of other people (some who are alumni mentors and others who are taking the same courses that you will be) in the ZTM Discord.

Best articles. Best resources. Only for ZTM subscribers.

If you enjoyed Travis's post and want to get more like it in the future, subscribe below. By joining over 300,000 ZTM email subscribers, you'll receive exclusive ZTM posts, opportunities, and offers.

No spam ever, unsubscribe anytime

More from Zero To Mastery

Beginner’s Guide To Python Automation Scripts (With Code Examples) preview
Beginner’s Guide To Python Automation Scripts (With Code Examples)
24 min read

Use Python automation scripts to save time, reduce errors, and boost productivity. Perfect for all skill levels. Work smarter, not harder!

How to Become a Full-Stack Web Developer & Get Hired in 2025 preview
How to Become a Full-Stack Web Developer & Get Hired in 2025
32 min read

Learn everything you need to know to become a Full-Stack Web Developer, as well as how to get hired as one in 2025 with this step-by-step guide!

Potential Python Interview Questions preview
Potential Python Interview Questions
15 min read

Are you taking a coding interview with Python? On rare occasions, you may be asked broad language questions before the technical interview: Here's 25 of them.