Deep Dive on Python - Introduction
October 13th 2025All there is to know about python
1. Objects
type(obj)- check the type of the objectisinstance(my_obj, MyClass)- check if the object is an instance of the class
1.1. Creating Classes
class MyClass:- Python creates and object
- called
MyClass - of type
type - with certain attributes automatically assigned
- called
- Python creates and object
class Person:
pass
type(Person) # type
type(type) # type
## Person.name # 'Person'
p = Person()
type(p) # __main__.Person
p.__class__ # __main__.Person
type(p) is p.__class__ # True
isinstance(p, Person) # True 1.2 Class Attributes
MyClassis aclass-> it is an object of typetype- We are talking about the object
MyClassitself not the instance ofMyClass
1.2.1 Retrieving Attribute Values from Objects
getattr(object_symbol: type, attr_name: str, default_return_val)can be used to retrieve attributes
class MyClass:
language = 'Python'
verison = '3.6'
getattr(MyClass, 'language') # 'Python'
getattr(MyClass, 'width', 'N/A') # 'N/A default value'
try:
getattr(MyClass, 'name') # AttributeError exception
except AttributeError:
print("attribute 'name' does't exist in class MyClass")
## dot notation
MyClass.language # 'Python'
try:
MyClass.x # AttributeError
except AttributeError:
print("attribute 'x' does't exist in class MyClass") 1.2.2 Setting Attributes Values
setattr(object_symbol: type, attribute_name: str, attribute_value)- can be used to set values
class MyClass:
language = 'Python'
version = '3.6'
setattr(MyClass, 'language', 'Java')
MyClass.version
getattr(MyClass, 'language') # Java 1.2.3 Where if the state of a class stored
class MyClass:
language = 'Python'
verison = '3.6'
print(MyClass.__dict__) # a read only hashmap, called class namespace Max
Max
Peter 1.2.4 Setting Attribute Value to a Callable
class MyClass:
def hello(): # a callable attribute value
pass
'''
{
...
'hello': <function MyClass.hello at 0x1032b3920>,
...
}
'''
MyClass.__dict__
1.2.5 Data Attributes, Classes & Instances
MyClass.__dict__- returns the attributes of theMyClassobject typemy_obj.__dict__- returns the attributes of themy_objinstance of theMyClassMyClass.language- Python looks for
languageattribute inMyClassnamespace
- Python looks for
my_obj.language- Python looks for
languageattribute inmy_objnamespace first - if not found, it looks for
languagein type(class) ofmy_obji.eMyClass
- Python looks for
class MyClass:
language = "Python"
my_obj = MyClass()
print(
MyClass.__dict__, # {'language': 'Python'}
my_obj.__dict__, # {}
MyClass.language, # Python
my_obj.language, # Python
sep="\n"
)
## Now this is called an instance attr not a class attr
## This only changes the instance attr, not class attr
my_obj.language = 'java'
print(
my_obj.__dict__,
MyClass.__dict__,
sep="\n"
) {'__module__': '__main__', '__firstlineno__': 80, 'language': 'Python', '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None}
{}
Python
Python 1.2.6 Function Attribute of a class
- When attributes are function they behave different then regular attributes
class MyClass:
def say_hello():
print("Hello World!")
my_obj = MyClass()
print(
MyClass.say_hello, # <function MyClass.say_hello at 0x1006f7880>
my_obj.say_hello, # <bound method MyClass.say_hello of <__main__.MyClass object at 0x10059ba10>>
sep="\n"
)
MyClass.say_hello()
try:
my_obj.say_hello()
except TypeError as e:
print("Error:", e) <function MyClass.say_hello at 0x1028bf880>
<bound method MyClass.say_hello of <__main__.MyClass object at 0x1026b3a10>>
Hello World!
Error: MyClass.say_hello() takes 0 positional arguments but 1 was given 1.2.6 Methods
In
my_object.say_hello()say_hellois an object ofmethodtype- it is bound to the object
my_obj - when
my_obj.say_hello()is called, the bound objmy_objis injected as the first argument by default - Now
say_hello()has a handle on the namespace ofmy_obj- it can access the attributes defined in the namespace
Functions ==defined in the Class== - becomes a ==method== when called from an instance of that class
Function ==defined in the instance== at runtime, remains a ==regular function==.
class MyClass:
language = 'Python'
def say_hello(obj, name):
return f"Hello {name}! I am {obj.language}."
python = MyClass()
print(python.say_hello('John'))
java = MyClass()
java.language = 'Java'
print(java.say_hello('John'))
MyClass.do_something = lambda self: print(f"Doing something at {self}")
python.write_something = lambda self: print(f"Writing something at {self}")
print(
python.do_something, # function defined in the class, becomes method
python.write_something, # function defined in the instance, stays a regular function
sep="\n"
) Hello John! I am Python.
Hello John! I am Java.
<bound method <lambda> of <__main__.MyClass object at 0x102812ba0>>
<function <lambda> at 0x1029c3c40> 1.3 Initializing a class instance
- When
MyClass('3.13')is called- Python create a new instance object
objwith ==empty namespace== - If
__init__is defined,- it calls
obj.__init__('3.13')- which isMyClass.__init__(obj, '3.13) - the function then add
versionto the empty namespace ofobjand assigns it to3.13 versionis an instance attr ofobjobj.__dict__->{'version': '3.13'}
- it calls
- Python create a new instance object
class MyClass:
language = 'Python'
def __init__(obj, version):
obj.version = version
1.3.1 Difference between __new__ and __init__
- By the time
__init__is called- new instance
objhas already been created with an empty namespace - then
__init__is called as a bound method to thatobj
- new instance
__new__can be used to override the default object creation behavior - before the initialization
1.4 Creating Attributes at Runtime
- We saw in [[OOP - Introduction#1.2.6.i Methods]]
- functions attributes created on an instance at runtime is not a method, they are regular functions
- but we can define them as such to make them a method
1.4.1 Creating and Binding a method to an instance at runtime
from types import MethodType # MethodType(function , object)
class MyClass:
language = 'Python'
def __init__(obj, version):
obj.version = version
obj = MyClass('3.13')
obj.do_something = lambda self: print("hello world!")
print(obj.do_something)
try:
obj.do_something_meth = MethodType(lambda self: print("hello world!"), obj)
print(obj.do_something_meth)
except:
print("this doesnot work in a notebook for some reason!")
<function <lambda> at 0x10442ca40>
<bound method <lambda> of <__main__.MyClass object at 0x104206a50>> 1.5 Class and Instance Properties
1.5.1 Defining properties using “property” type
- class has
getter,setter,deleter propertyis an immutable object
class MyClass:
def __init__(self, val):
self._prop = val
def get_prop(self):
print("getting prop")
return self._prop
def set_prop(self, val):
print("setting prop")
self._prop = val
prop = property(get_prop) # defining getter
prop = prop.setter(set_prop) # defining setter
obj = MyClass(10)
obj.prop
obj.prop = 20 getting prop
setting prop 1.5.2 Using the “property” type as a decorator
class MyClass:
def __init__(self, val):
self._prop = val
'''
What is happening here?
1. prop method gets defined - "prop" holds the reference to the method object
2. prop is passed into property() as an arg and the resulting value is assigned to the "prop" var
prop <property type> = property(prop <method prop(self)>)
'''
@property
def prop(self):
return self._prop
'''
What is happening here?
prop <property type> = prop.setter(prop <method prop(self, val)>)
'''
@prop.setter
def prop(self, val):
self._prop = val
obj = MyClass(10)
obj.prop
obj.prop = 20 1.5.3 Immutability of “Property” object
def getter():
pass
def setter():
pass
p = property()
p = p.getter(getter)
print("p getter:", id(p))
p = p.setter(setter)
print("p setter:", id(p)) # property object's internal state is not changed - but rather a new property object is created and bound to "p" p getter: 4339486768
p setter: 4339486528 1.5.4 Read Only and Computed Property
1.5.4.i Read-only Property
- not true read only, since
_propis still accessible - just a suggestion
- just define a getter and not the setter
1.5.4.ii Computed Property
import math
class Circle:
def __init__(self, radius):
self._radius = radius
self._area = None
def area(self):
print("regular method")
return math.pi * (self._radius ** 2)
@property
def area_prop(self):
print("property")
return math.pi * (self._radius ** 2)
@property
def area_cached(self):
if self._area is None:
print("calc in lazy")
self._area = math.pi * (self._radius ** 2)
return self._area
else:
print("pulled from cache")
return self._area
circle = Circle(10)
circle.area()
circle.area_prop
circle.area_cached
circle.area_cached regular method
property
calc in lazy
pulled from cache
314.1592653589793 1.5.4.iii Example webpage loader
from urllib import request
from time import perf_counter
class WebPage():
def __init__(self, url):
self.url = url
self._page = None
self._page_size = None
self._page_load_time_sec = None
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value
@property
def page(self):
if self._page is None:
self._download_page()
return self._page
@property
def page_size(self):
if self._page is None:
self._download_page()
return self._page_size
@property
def time_elapsed(self):
if self._page is None:
self._download_page()
return self._page_load_time_sec
def _download_page(self):
"downlad the page & and set all the instance props related to that page in that function"
start_time = perf_counter()
with request.urlopen(self.url) as f:
self._page = f.read()
end_time = perf_counter()
self._page_size = len(self._page)
self._page_load_time_sec = end_time - start_time
urls = [
# "https://www.python.org",
# "https://www.github.com",
# "https://www.w3schools.com",
# "https://docs.python.org/3.13/"
]
for url in urls:
web_page = WebPage(url)
print( f"{url}\tsize={web_page.page_size}\telapsed={web_page.time_elapsed:.2f} secs")
https://www.python.org size=50110 elapsed=0.21secs
https://www.github.com size=560652 elapsed=0.99secs
https://www.w3schools.com size=423163 elapsed=0.53secs
https://docs.python.org/3.13/ size=17840 elapsed=0.14secs 1.5.4 Deleting Properties (from instance)
class MyClass:
def __init__(self, language="Python"):
self.language = language
@property
def language(self):
return self._language
@language.setter
def language(self, val):
self._language = val
@language.deleter
def language(self):
del self._language
my_class = MyClass()
print(my_class.language)
'''
OR
delattr(my_class, "language")
'''
del my_class.language # this is just modifying instance not the class
try:
print(my_class.language)
except AttributeError as e:
print(e)
Python
'MyClass' object has no attribute '_language' 1.5.5 Class and Static Methods
- A function defined inside a class will alter its behavior based on how it’s called
class MyClass:
def hello():
print("Hello")
MyClass.hello()
try:
m = MyClass()
m.hello() # Not allowed in m.hello() - hello() is bound to the class
except TypeError as e:
print(e) Hello
MyClass.hello() takes 0 positional arguments but 1 was given - but how to create a function that is always bound to the class
1.5.5.i Class Methods - a function that is always bound to the class
- It can be done using
@classmethod
class MyClass:
def hello():
print("hello..")
def hello_inst(self):
print(f"hello from self: {self}, {id(self)}")
@classmethod
def hello_cls(cls):
'''
- Before hello_cls() is passed into the decorator, it is a regular function
- After the hello_cls() is passed into the decorator, it becomes a method for the MyClass obj, and not the instance
'''
print(f"hello from {cls}, {id(cls)}")
MyClass.hello()
c = MyClass()
c.hello_inst()
MyClass.hello_cls()
c.hello_cls()
hello..
hello from self: <__main__.MyClass object at 0x100aa5fd0>
hello from <class '__main__.MyClass'>
hello from <class '__main__.MyClass'> | function | MyClass | Instance |
|---|---|---|
| hello | regular function | method bound to instance - will fail |
| hello_inst | regular function | method bound to instance |
| hello_cls | method bound to class | method bound to class |
1.5.5.ii Static Methods - a function that is never bound to any object
- it can be done using
@staticmethod
class MyClass:
@staticmethod
def hello():
print("hello..")
MyClass.hello()
c = MyClass()
c.hello()
## both are of the same type
print(MyClass.hello)
print(c.hello) hello..
hello..
<function MyClass.hello at 0x1036e2480>
<function MyClass.hello at 0x1036e2480> | function | MyClass | Instance |
|---|---|---|
| hello | regular function | regular function |
1.6 Builtin types & Standard types in Python - import types
- Some types defined in python as a part of built-in
- int, str, list, tuple ,…
- When you do
type([1,2,3])-<class 'list'> - But not all do, so we use
import types
1.7 Class Body Scope and Class function/method scopes
==Module scope== contains -
Python,p==Classbody scope== contains -
kingdom,pythlum,family,__init__,say_hello__init__&say_hellosymbols are in the class body namespace.But the scope of functions referenced by
__ini__&say_helloare different then regular nested scopes.__ini__&say_hellofunctions are not nested inside the ==classbody scope==- they are nested inside the ==module scope==
- When Python looks for a symbol in a function inside a class
- It will not use the ==Classbody scope==!
first_name = "Peter"
class Person:
first_name = "Max"
last_name = "Evans"
'''
this works because current scope has both first_name & last_name
'''
full_name = first_name + last_name
def first_name_inst(self):
return self.first_name
@classmethod
def first_name_cls(cls):
return cls.first_name
# it will return 'Peter' because it is not nested inside the classbody scope
# and pulls the value from the module scope
def first_name_direct():
return first_name
p = Person()
print(
p.first_name_inst(),
Person.first_name_cls(),
Person.first_name_direct(),
sep="\n"
)
Max
Max
Peter