Object-Oriented Python: Classes and Objects

Published on: July 10, 2026
Reading time: 6 minutes
logo do python com objetos abaixo do logo

Object-oriented Python organizes a program around objects that combine data and behavior. A bank account can store a balance and provide deposit or withdrawal methods. A product can store a name and price and calculate a discounted value. A game character can hold health points and perform actions.

Python supports object-oriented programming without requiring every problem to use it. This guide explains classes, objects, attributes, methods, constructors, class attributes, properties, inheritance, composition, encapsulation, and practical design choices.

What is an object?

An object is a value with state and behavior. Python already provides many objects:

name = "Python"
numbers = [3, 1, 2]

print(name.upper())
numbers.sort()
print(numbers)

The string object provides upper(), and the list object provides sort(). Their methods operate on the object’s data. The official Python classes tutorial explains the language model for classes, instances, methods, and inheritance.

Class and instance

A class defines the structure and behavior of a type. An instance is one concrete object created from that class.

class Product:
    pass


book = Product()
course = Product()

print(type(book))
print(book is course)  # False

book and course are distinct instances, even though both belong to the Product class.

Initialize objects with __init__

The __init__ method initializes instance data after an object is created:

class Product:
    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price


book = Product("Python Handbook", 39.90)
print(book.name)
print(book.price)

The first parameter is conventionally named self. It refers to the current instance. When Python executes Product("Python Handbook", 39.90), it creates an object and passes that object to __init__ as self.

Instance attributes

self.name and self.price are instance attributes. Each object has its own values:

book = Product("Python Handbook", 39.90)
course = Product("Python Fundamentals", 89.00)

print(book.name)    # Python Handbook
print(course.name)  # Python Fundamentals

Attributes should represent state that belongs to the object. A local variable inside a method disappears after that method returns, while an instance attribute remains attached to the instance.

Instance methods

A method is a function defined inside a class:

class Product:
    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price

    def apply_discount(self, percentage: float) -> float:
        if not 0 <= percentage <= 100:
            raise ValueError("percentage must be between 0 and 100")
        return self.price * (1 - percentage / 100)


book = Product("Python Handbook", 40.00)
print(book.apply_discount(15))

Calling book.apply_discount(15) automatically supplies book as self. Validation keeps invalid state or arguments from silently producing misleading results. See the Python exception handling guide for error design.

Represent objects clearly with __repr__

class Product:
    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price

    def __repr__(self) -> str:
        return f"Product(name={self.name!r}, price={self.price!r})"


book = Product("Python Handbook", 39.90)
print(book)

A useful __repr__ makes debugging and logs easier. The f-string debugging guide explains !r and expression labels.

Class attributes

A class attribute is shared through the class rather than initialized separately for each object:

class Product:
    currency = "USD"

    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price


book = Product("Python Handbook", 39.90)
course = Product("Python Fundamentals", 89.00)

print(book.currency)
print(course.currency)

Class attributes work well for constants or defaults shared by all instances. Avoid placing a mutable list or dictionary there unless shared state is explicitly intended.

The mutable class attribute trap

class Cart:
    items = []  # Shared by every instance: usually a bug.

Both carts would refer to the same list. Create mutable instance state inside __init__:

class Cart:
    def __init__(self) -> None:
        self.items = []

Class methods

A class method receives the class as its first argument and can provide an alternative constructor:

class Product:
    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price

    @classmethod
    def from_string(cls, value: str):
        name, price_text = value.rsplit(",", maxsplit=1)
        return cls(name.strip(), float(price_text))


product = Product.from_string("Python Handbook, 39.90")
print(product)

Using cls rather than writing Product directly supports subclasses correctly.

Static methods

A static method belongs conceptually to the class but does not need an instance or class reference:

class Product:
    @staticmethod
    def is_valid_price(value: float) -> bool:
        return value >= 0


print(Product.is_valid_price(10))
print(Product.is_valid_price(-2))

Use static methods sparingly. If a function does not strongly belong to the class, a normal module-level function may be clearer.

Encapsulation in Python

Encapsulation means keeping state and rules together behind a useful interface. Python relies more on conventions than strict private access modifiers:

  • name is public;
  • _name signals an internal implementation detail;
  • __name triggers name mangling, mainly to avoid accidental conflicts in subclasses.

An underscore is not a security boundary. It communicates that callers should use the public methods or properties instead.

Validate attributes with properties

class Product:
    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price

    @property
    def price(self) -> float:
        return self._price

    @price.setter
    def price(self, value: float) -> None:
        if value < 0:
            raise ValueError("price cannot be negative")
        self._price = float(value)


book = Product("Python Handbook", 39.90)
book.price = 42
print(book.price)

The caller uses normal attribute syntax while the class enforces a rule. Do not add properties automatically to every field; direct public attributes are appropriate when no additional behavior is needed.

Inheritance

Inheritance creates a specialized class based on another class:

class Employee:
    def __init__(self, name: str) -> None:
        self.name = name

    def describe_role(self) -> str:
        return "Employee"


class Developer(Employee):
    def __init__(self, name: str, language: str) -> None:
        super().__init__(name)
        self.language = language

    def describe_role(self) -> str:
        return f"Developer working with {self.language}"


developer = Developer("Jordan", "Python")
print(developer.name)
print(developer.describe_role())

super() calls the parent implementation. The subclass overrides describe_role() with more specific behavior.

Polymorphism

Polymorphism lets different object types respond to the same operation:

class EmailNotification:
    def send(self, message: str) -> None:
        print(f"Email: {message}")


class SmsNotification:
    def send(self, message: str) -> None:
        print(f"SMS: {message}")


def notify(channel, message: str) -> None:
    channel.send(message)


notify(EmailNotification(), "Order shipped")
notify(SmsNotification(), "Order shipped")

The notify() function depends on the behavior it needs, not a specific class hierarchy. This style is often called duck typing: if an object provides the expected method, it can participate.

Composition

Composition builds an object from other objects. It often models relationships more naturally than inheritance:

class Engine:
    def start(self) -> str:
        return "Engine started"


class Car:
    def __init__(self, model: str, engine: Engine) -> None:
        self.model = model
        self.engine = engine

    def start(self) -> str:
        return f"{self.model}: {self.engine.start()}"


car = Car("City Car", Engine())
print(car.start())

A car has an engine, so composition expresses the relationship clearly. Inheritance is better reserved for a genuine “is a” relationship with substitutable behavior.

Dataclasses for data-focused objects

When a class mainly stores data, the standard-library dataclasses module reduces boilerplate:

from dataclasses import dataclass


@dataclass
class Customer:
    name: str
    email: str
    active: bool = True


customer = Customer("Taylor", "[email protected]")
print(customer)

Python generates methods such as __init__ and __repr__. The official dataclasses documentation explains defaults, ordering, frozen instances, and post-initialization.

Example project: bank account

class BankAccount:
    def __init__(self, owner: str, initial_balance: float = 0) -> None:
        if initial_balance < 0:
            raise ValueError("initial balance cannot be negative")
        self.owner = owner
        self._balance = float(initial_balance)

    @property
    def balance(self) -> float:
        return self._balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def withdraw(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("withdrawal must be positive")
        if amount > self._balance:
            raise ValueError("insufficient funds")
        self._balance -= amount

    def __repr__(self) -> str:
        return (
            f"BankAccount(owner={self.owner!r}, "
            f"balance={self.balance:.2f})"
        )

The class protects its rules: callers cannot withdraw more than the balance through the public method, and deposits must be positive.

Test object behavior

import pytest


def test_deposit_increases_balance():
    account = BankAccount("Taylor", 100)
    account.deposit(25)
    assert account.balance == 125


def test_withdraw_rejects_insufficient_funds():
    account = BankAccount("Taylor", 50)

    with pytest.raises(ValueError, match="insufficient funds"):
        account.withdraw(75)

The Pytest guide explains fixtures, exception tests, and test organization. Tests should focus on observable behavior rather than internal implementation details.

Document classes and methods

class BankAccount:
    """Represent an account with validated deposit and withdrawal operations."""

    def deposit(self, amount: float) -> None:
        """Add a positive amount to the account balance."""
        ...

The Python docstrings guide covers class, method, parameter, return, and exception documentation.

Organize classes into modules

As a project grows, separate responsibilities:

shop/
├── __init__.py
├── models/
│   ├── product.py
│   ├── customer.py
│   └── order.py
├── services/
│   └── checkout.py
└── tests/
    ├── test_product.py
    └── test_order.py

A class should have a focused responsibility. A single class that manages users, databases, email, reports, and payments is difficult to test and change.

When object-oriented design helps

Classes are useful when:

  • multiple values and operations belong to one concept;
  • several independent instances must maintain their own state;
  • rules should be enforced whenever state changes;
  • different implementations share an interface;
  • composition models a system of cooperating parts.

When a class is unnecessary

A small calculation or stateless transformation may be clearer as a function:

def celsius_to_fahrenheit(celsius: float) -> float:
    return celsius * 9 / 5 + 32

Do not create a class merely to hold one unrelated function. Python supports procedural, functional, and object-oriented styles, and a project can combine them.

Common beginner mistakes

  • Forgetting self: instance methods receive the current object as the first parameter.
  • Using class attributes for mutable instance data: lists and dictionaries can be shared accidentally.
  • Creating deep inheritance hierarchies: composition is often easier to understand and change.
  • Writing getters and setters automatically: public attributes and properties are more idiomatic when appropriate.
  • Letting one class do everything: split unrelated responsibilities.
  • Exposing invalid state: validate at object boundaries.
  • Testing private details: test public behavior so implementation can evolve.
  • Confusing identity and equality: the Python == versus is guide explains value and object identity.

A practical design checklist

  1. Name the real concept represented by the class.
  2. List the state each instance owns.
  3. Define the operations that preserve its rules.
  4. Keep constructor validation clear.
  5. Prefer composition unless inheritance expresses a genuine substitutable relationship.
  6. Expose a small public interface.
  7. Add useful representations and type hints.
  8. Test normal behavior, boundaries, and invalid operations.
  9. Refactor when a class gains unrelated responsibilities.

Conclusion

Object-oriented Python combines state and behavior through classes and instances. Begin with __init__, instance attributes, and methods. Add properties when validation or computed access is needed, use class methods for alternative constructors, choose composition for “has a” relationships, and apply inheritance only where substitution makes sense. The goal is not to maximize the number of classes, but to create code whose concepts, rules, and responsibilities are easy to understand and test.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Programação assíncrona com asyncio em Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python asyncio: A Practical Beginner’s Guide

    Learn Python asyncio with coroutines, await, tasks, TaskGroup, timeouts, cancellation, queues, locks, error handling, and blocking work.

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    Uso de expressões regulares regex para manipulação de texto em Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Regex: Complete Regular Expressions Guide

    Learn Python regex with re.search, fullmatch, findall, groups, quantifiers, substitutions, flags, compiled patterns, and practical examples.

    Ler mais

    Tempo de leitura: 7 minutos
    10/07/2026
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Decorators Explained: Practical Guide

    Learn Python decorators with practical examples, wrappers, arguments, functools.wraps, class decorators, common use cases, and mistakes to avoid.

    Ler mais

    Tempo de leitura: 8 minutos
    21/05/2026
    Uso do super em Python para resolver problemas de herança
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Use super() in Python and Fix Inheritance Errors

    Learn how to use super() in Python for parent methods, multiple inheritance, MRO, *args, **kwargs, and cleaner object-oriented code.

    Ler mais

    Tempo de leitura: 8 minutos
    19/05/2026
    Criando instalador EXE com ícone personalizado em Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    How to Create a exe Installer with a Custom Icon in Python

    Learn how to package any Python script into a standalone .exe file with a custom icon using PyInstaller. Step-by-step guide

    Ler mais

    Tempo de leitura: 11 minutos
    09/05/2026