Skip to main content

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects that contain both data (attributes) and behavior (methods). Python’s OOP features make it easy to write maintainable, reusable code.

Classes and Objects

Defining a Class

A class is a blueprint for creating objects. Let’s look at a real example from a contact management system:
class Contact:
    """
    Clase que representa un contacto.
    
    Atributos:
        nombre (str): Nombre del contacto
        telefono (str): Teléfono del contacto
        email (str): Email del contacto
        direccion (str): Dirección del contacto
    """
    
    def __init__(self, nombre: str, telefono: str, email: str, direccion: str):
        self._nombre = nombre
        self._telefono = telefono
        self._email = email
        self._direccion = direccion
The __init__ method is a special method called a constructor. It runs automatically when you create a new instance of the class.

Creating Objects

Once you have a class, you can create instances (objects) of it:
# Creating a new contact
contacto = Contact(
    "Juan García",
    "+56912345678",
    "[email protected]",
    "Calle Principal 123"
)

print(contacto.nombre)  # Access attribute

Encapsulation

Private Attributes

In Python, attributes prefixed with an underscore (_) are considered private by convention:
class Contact:
    def __init__(self, nombre: str, telefono: str, email: str, direccion: str):
        self._nombre = nombre        # Private attribute
        self._telefono = telefono    # Private attribute
        self._email = email          # Private attribute
        self._direccion = direccion  # Private attribute
The underscore prefix indicates that these attributes should not be accessed directly from outside the class.

Properties (Getters and Setters)

Python uses the @property decorator to create getters and setters:
class Contact:
    # ... __init__ method ...
    
    @property
    def nombre(self) -> str:
        return self._nombre
    
    @property
    def telefono(self) -> str:
        return self._telefono
    
    @property
    def email(self) -> str:
        return self._email
    
    @property
    def direccion(self) -> str:
        return self._direccion
Type Hints: The -> str syntax indicates the return type of the method. While optional, type hints improve code readability and enable better IDE support.

Special Methods (Dunder Methods)

Special methods (also called magic methods or dunder methods) allow you to define how objects behave with built-in Python operations:
These methods control how objects are converted to strings:
class Contact:
    # ... other methods ...
    
    def __str__(self) -> str:
        """User-friendly string representation"""
        return (
            f"Nombre: {self._nombre}\n"
            f"Teléfono: {self._telefono}\n"
            f"Email: {self._email}\n"
            f"Dirección: {self._direccion}"
        )
    
    def __repr__(self) -> str:
        """Developer-friendly representation"""
        return f"Contact('{self._nombre}', '{self._telefono}', '{self._email}', '{self._direccion}')"

# Usage
contacto = Contact("Juan García", "+56912345678", "[email protected]", "Calle 123")

print(contacto)  # Uses __str__
# Output:
# Nombre: Juan García
# Teléfono: +56912345678
# Email: [email protected]
# Dirección: Calle 123

print(repr(contacto))  # Uses __repr__
# Output: Contact('Juan García', '+56912345678', '[email protected]', 'Calle 123')
MethodPurposeExample
__init__Constructorobj = MyClass()
__str__String representationstr(obj), print(obj)
__repr__Developer representationrepr(obj)
__len__Lengthlen(obj)
__eq__Equalityobj1 == obj2
__lt__Less thanobj1 < obj2
__add__Additionobj1 + obj2

Instance Methods vs Static Methods

Instance methods operate on an instance of the class and have access to self:
class Contact:
    def __init__(self, nombre: str, telefono: str, email: str, direccion: str):
        self._nombre = nombre
        self._telefono = telefono
        self._email = email
        self._direccion = direccion
    
    def to_dict(self) -> dict:
        """Convert contact to dictionary"""
        return {
            "nombre": self._nombre,
            "telefono": self._telefono,
            "email": self._email,
            "direccion": self._direccion,
        }

# Usage
contacto = Contact("Juan", "+56912345678", "[email protected]", "Calle 123")
datos = contacto.to_dict()
print(datos)

Complex Classes: Contact Manager

Let’s look at a more complex class that manages a collection of contacts:
import json
import os
from contact import Contact

class ContactManager:
    """
    Gestor de contactos.
    
    Responsabilidades:
    - Agregar, editar y eliminar contactos
    - Buscar contactos por nombre o teléfono
    - Persistencia de datos en JSON
    - Gestión de la lista de contactos
    """
    
    def __init__(self, archivo_datos: str = "contactos.json"):
        self._archivo_datos = archivo_datos
        self._contactos: list[Contact] = []
        self._cargar_contactos()
    
    def _cargar_contactos(self):
        """Carga los contactos desde el archivo JSON"""
        if os.path.exists(self._archivo_datos):
            try:
                with open(self._archivo_datos, "r", encoding="utf-8") as archivo:
                    datos = json.load(archivo)
                    self._contactos = [Contact.from_dict(c) for c in datos]
                print(f"✅ {len(self._contactos)} contactos cargados correctamente")
            except Exception as e:
                print(f"⚠️  Error al cargar contactos: {e}")
                self._contactos = []
        else:
            self._contactos = []
    
    def _guardar_contactos(self):
        """Guarda los contactos en el archivo JSON"""
        try:
            datos = [c.to_dict() for c in self._contactos]
            with open(self._archivo_datos, "w", encoding="utf-8") as archivo:
                json.dump(datos, archivo, indent=2, ensure_ascii=False)
        except Exception as e:
            raise Exception(f"Error al guardar contactos: {e}")
Private Methods: Methods prefixed with _ (like _cargar_contactos) are internal helper methods not meant to be called from outside the class.

CRUD Operations

The ContactManager class implements Create, Read, Update, and Delete operations:
class ContactManager:
    # ... other methods ...
    
    def agregar_contacto(self, nombre: str, telefono: str, email: str, direccion: str):
        """Agrega un nuevo contacto"""
        if self.buscar_por_telefono(telefono):
            raise ValueError(f"Ya existe un contacto con el teléfono '{telefono}'")
        
        nuevo_contacto = Contact(nombre, telefono, email, direccion)
        self._contactos.append(nuevo_contacto)
        self._guardar_contactos()

# Usage
gestor = ContactManager()
gestor.agregar_contacto(
    "Ana Silva",
    "+56912345678",
    "[email protected]",
    "Paseo del Mar 101"
)
class ContactManager:
    # ... other methods ...
    
    def buscar_por_nombre(self, nombre: str) -> list[Contact]:
        """Busca contactos por nombre (búsqueda parcial)"""
        nombre_busqueda = nombre.lower()
        return [c for c in self._contactos if nombre_busqueda in c.nombre.lower()]
    
    def buscar_por_telefono(self, telefono: str) -> Contact | None:
        """Busca un contacto por teléfono (búsqueda exacta)"""
        for contacto in self._contactos:
            if contacto.telefono == telefono:
                return contacto
        return None
    
    def obtener_todos_contactos(self) -> list[Contact]:
        """Obtiene todos los contactos"""
        return self._contactos

# Usage
resultados = gestor.buscar_por_nombre("Ana")
for contacto in resultados:
    print(contacto)
class ContactManager:
    # ... other methods ...
    
    def editar_contacto(self, telefono: str, nombre: str = None, 
                       email: str = None, direccion: str = None):
        """Edita un contacto existente"""
        contacto = self.buscar_por_telefono(telefono)
        if not contacto:
            raise ValueError(f"No existe contacto con el teléfono '{telefono}'")
        
        if nombre:
            contacto.nombre = nombre
        if email:
            contacto.email = email
        if direccion:
            contacto.direccion = direccion
        
        self._guardar_contactos()

# Usage
gestor.editar_contacto(
    "+56912345678",
    nombre="Ana María Silva",
    email="[email protected]"
)
class ContactManager:
    # ... other methods ...
    
    def eliminar_contacto(self, telefono: str):
        """Elimina un contacto"""
        contacto = self.buscar_por_telefono(telefono)
        if not contacto:
            raise ValueError(f"No existe contacto con el teléfono '{telefono}'")
        
        self._contactos.remove(contacto)
        self._guardar_contactos()

# Usage
gestor.eliminar_contacto("+56912345678")
print("Contacto eliminado")

OOP Best Practices

Single Responsibility

Each class should have one clear purpose.
# Good: Separate concerns
class Contact:  # Data model
    pass

class ContactManager:  # Business logic
    pass

Encapsulation

Keep attributes private and use properties for controlled access.
class Contact:
    def __init__(self, nombre):
        self._nombre = nombre
    
    @property
    def nombre(self):
        return self._nombre

Use Type Hints

Type hints improve code readability and IDE support.
def buscar_por_nombre(self, 
                     nombre: str) -> list[Contact]:
    return [c for c in self._contactos]

Document Your Classes

Use docstrings to explain class purpose and methods.
class ContactManager:
    """
    Gestor de contactos.
    
    Responsabilidades:
    - Agregar, editar y eliminar contactos
    - Buscar contactos por nombre o teléfono
    """

Real-World Example

Here’s how these classes work together in a real application:
# Initialize the contact manager
gestor = ContactManager("contactos.json")

# Add contacts
gestor.agregar_contacto(
    "Juan García",
    "+56912345678",
    "[email protected]",
    "Calle Principal 123"
)

gestor.agregar_contacto(
    "María López",
    "+56987654321",
    "[email protected]",
    "Avenida Central 456"
)

# Search for contacts
resultados = gestor.buscar_por_nombre("Juan")
for contacto in resultados:
    print(contacto)

# Update a contact
gestor.editar_contacto(
    "+56912345678",
    email="[email protected]"
)

# Get all contacts
todos = gestor.obtener_todos_contactos()
print(f"\nTotal de contactos: {gestor.obtener_cantidad_contactos()}")

Practice Exercises

Create a Book class with:
  • Attributes: title, author, isbn, price
  • Properties with validation
  • Methods: to_dict(), from_dict(), apply_discount(percentage)
  • Special methods: __str__ and __repr__
Create a LibraryManager class that:
  • Manages a collection of books
  • Can add, remove, and search books
  • Saves/loads books to/from JSON
  • Calculates total library value
Extend the Contact class to create:
  • BusinessContact (with company and job title)
  • PersonalContact (with birthday and relationship)
Both should inherit from Contact and add their specific attributes.

Next Steps

Python Projects

Apply OOP concepts in real-world projects

Python Basics

Review Python fundamentals

Build docs developers (and LLMs) love