Python – Object Oriented Programming – Design Inheritance & Properties

Best Practices

Designing for Inheritance and Polymorphism




# Withdraw amount from each account in list_of_accounts
def batch_withdraw(list_of_accounts, amount):
  for acct in list_of_accounts:
b, c, s = BankAccount(1000), CheckingAccount(2000), SavingsAccount(3000)
batch_withdraw([b,c,s])   # uses bankaccount.withdraw, checking, Savings
  • batch_withdraw() doesn’t need to check the object to know which withdraw() to call

Liskov substitution principle

Base class should be interchangeable with any of its subclasses without altering any properties of the program

  • Wherever BankAccout woorks –> CheckingAccount should work as well
  • Syntactically
    • function signatures are compatible –> arguments, returned values
  • Semantically
    • the state of the object and the program remains consistent
      • subclass method doesn’t strengthen input conditions
      • subclass method doesn’t weaken output conditions
      • no additional exceptions


Violating LSP

  • Syntactic incompatibility
    • BankAccount.withdraw() requires 1 parameter but CheckingAccount.withdraw() requieres 2
  • Subclass stengthening input conditions
    • BankAccount.withdraw() accepts any amount, but CheckingAccount.withdraw() assumes that the amount is limited
  • Subclass weakening output conditions
    • BankAccount.withdraw() can only leave a positive balance or cause an error,
    • CheckingAccount.withdraw() can leave balance negative
  • Chaning additional attributes in subclass’s method
  • Throwing additional exceptions in subclass’s method

NO LSP – No Inheritance


class Parent:
  def talk(self):
    print("Parent talking!")     
class Child(Parent):
  def talk(self):
    print("Child talking!")          
class TalkativeChild(Parent):
  def talk(self):
    print("TalkativeChild talking!")
p, c, tc = Parent(), Child(), TalkativeChild()
for obj in (p, c, tc):

> Parent talking!
Child talking!
TalkativeChild talking!
Parent talking!


# Define a Rectangle class
class Rectangle:
    def __init__(self, h, w):
      self.h, self.w = h, w

# Define a Square class
class Square(Rectangle):
    def __init__(self, w):
      self.h, self.w = w, w
class Rectangle:
    def __init__(self, w,h):
      self.w, self.h = w,h
# Define set_h to set h       
    def set_h(self, h):
      self.h = h

# Define set_w to set w
    def set_w(self, w):
      self.w = w   
class Square(Rectangle):
    def __init__(self, w):
      self.w, self.h = w, w 
# Define set_h to set w and h 
    def set_h(self, h):
      self.h = h
      self.w = h
# Define set_w to set w and h 
    def set_w(self, w):
      self.w = w   
      self.h = w


Managing Data Access – private attributes

All class data is public

  • any method or atttribute from a class can be accessed, by design

Restricting Access

  • naming conventions
  • use @property to customize access
  • Overriding __getattr__() and __setattr__() 

Naming convention – internal attributes

obj.att_name , obj._method_name()

  • starts with a single _ –> “internal”
  • widely accepted in Python community
  • Not a part of the public API
  • As a class user –> don’t mess with it
  • As a class developer –> use for implementation detail & helper functions, …
    • df._is_mixed_type –> indicator if df is of a mixed type
    • datetime._ymd2ord() –>  method for ordering dates

Naming convention – pseudoprivate attributes

obj.__attr_name, obj.__method_name()

  • starts but doesn’t end with __ –> “private’
  • not inherited
  • Name mangling: obj.__attr_name is interpreted as  obj._MyClass__attr_name
    • obj._MyClass__attr_name  =  actual name of the attribute
  • Use : prevent name clashes in inherited classes

!! Leading and trailing __ are only used for built-in Python methods –> __init__(), __repr__() !!

  • _name –> helper method that checks validity of an attribute value but isn’t considered a part of the class public interface
  • __name –> a ‘version’ attribute that stores the actual version of the class and shouldn’t be passed to child classes which have their own versions
  • __name__ –> a method run whenever the object is printed
class BetterDate:
  _MAX_DAYS = 30
  _MAX_MONTHS = 12
  def __init__(self, year, month, day):
    self.year, self.month, = year, month, day
  def from_str(cls, datestr):
    year, month, day = map(int, datestr.split("-"))
    return cls(year, month, day)
   def _is_valid(self):
     return ( <= BetterDate._MAX_DAYS) and \
            (self.month <= BetterDate._MAX_MONTHS)
bd1 = BetterDate(2020, 4, 30)
> True
bd2 = BetterDate(2020, 6, 45)
> False



Changing attribute values

class Employee:
  def set_name(self, name): = name
  def set_salary(self, salary):
    self.salary = salary
  def give_raise(self, amount):
    self.salary = self.salary + amount
  def __init__(self, name, salary):, self.salary = name, salary

emp = Employee("Anton", 31000)
# Use dot syntax and = to alter atributes
emp.salary = emp.salary + 2000

Control attribute access ?

  • check the value for validity
  • make attributes read-only
    • modifying set_salary() doesn’t prevent emp.salary = -100
    • dot.syntax –> still access


Restricted and read-only attributes

import pandas as pd
df = pd.DataFrame({"colA": [1,2], "colB":[3,4]})
> colA colB
0    1    3
1    2    4

df.columns = ["new_colA", "new_colB"]
> new_colA new_colB
0    1    3
1    2    4

# will cause an error
df.columns = ["new_colA", "new_colB", "extra"]
>  ValueError: Length mismatch:
   Expected axis has 2 elements,
   new values have 3 elements

df.shape = (43, 27)
>  AttributeError: can't set attribute




class Employer:
  def __init__(self, name, new_salary):
    self._salary = new_salary

  def salary(self):
    return self._salary

  def salary(self, new_salary):
    if new_salary < 0:
      raise ValueError("Invalid salary")
    self._salary = new_salary
  • Use protected attribute with leading _ to store data
  • Use @property on a method whose name is exactly the name of the restricted attribute – return the internal attribute
  • Use @attr.setter on a method attr() that will be called on obj.attr = value
    • value to assign passed as argument
emp = Employee("Harry", 5000)
# accessing the "property"

emp.salary = 6000 # <-- @salary.setter

emp.salary = -1000
> ValueError: Invalid salary


Why use @property?

  • User-facing : behaving like attributes
  • Developer-facing : give control of access

Other possibilities

  • Do not add @attr.setter
    • Create a read-only property
  • Add @attr.getter
    • Use for method that is called when property’s value is retrieved
  • Add @attr.deleter
    • Use for the method that’s called when proporty is deleted using del
class Customer:
  def __init__(self, name, new_bal): = name
    if new_bal < 0:
      raise ValueError("Invalid balance!")
    self._balance = new_bal  
  def balance(self):
    return self._balance

  def balance(self, new_bal):
    if new_bal < 0:
      raise ValueError("Invalid balance!")
    self._balance = new_bal
    print("Setter method called")

cust = Customer("Belinda", 2000)
cust.balance = 3000
> Setter method Called


import pandas as pd
from datetime import datetime
class LoggedDF(pd.DataFrame):
  def __init__(self, *args, **kwargs):
    pd.DataFrame.__init__(self, *args, **kwargs)
    self._created_at =
  def to_csv(self, *args, **kwargs):
    temp = self.copy()
    temp["created_at"] = self._created_at
    pd.DataFrame.to_csv(temp, *args, **kwargs)   
  def created_at(self):
    return self._created_at

ldf = LoggedDF({"col1": [1,2], "col2":[3,4]})
    ldf.created_at = '2035-07-13'
except AttributeError:
    print("Could not set attribute")

> Could not set attribute