0% found this document useful (0 votes)
1 views25 pages

Python Cert Notes

Certification notes

Uploaded by

Pankaj Dhasmana
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views25 pages

Python Cert Notes

Certification notes

Uploaded by

Pankaj Dhasmana
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 25

The result produced by the division operator is always a float

when both ** arguments are integers, the result is an integer, too;


when at least one ** argument is a float, the result is a float, too.
This is very important: rounding always goes to the lesser integer. It means 1.5 is
1 and -1.5 is -2.
Operators: remainder (modulo) --The result of the operator is a remainder left
after the integer division.
% result could be INF or FLOAT.
print(-12 / 4.5)=-2.6666666666666665
print(-12 // 4.5)=-3.0
print(-12 % 4.5)=1.5
print (-12-(4.5*(-3.0)))=1.5

print(12 / -4.5)
print(12 // -4.5)
print(12 % -4.5)
print (12-(4.5*(-3.0)))
-2.6666666666666665
-3.0
-1.5
-1.5
print(9 % 6 % 2)=1 as This operator has left-sided binding
** the exponentiation operator uses right-sided binding.
Priority Operator
1 +, - unary
2 **
3 *, /, //, %
4 +, - binary
5 <, <=, >, >=
6 ==, !=

If you want to give a name to a variable, you must follow some strict rules:

the name of the variable must be composed of upper-case or lower-case letters,


digits, and the character _ (underscore)
the name of the variable must begin with a letter;
the underscore character is a letter;
upper- and lower-case letters are treated as different (a little differently than
in the real world - Alice and ALICE are the same first names, but in Python they
are two different variable names, and consequently, two different variables);
the name of the variable must not be any of Python's reserved words (the keywords -
we'll explain more about this soon).

Take a look at the list of words that play a very special role in every Python
program.

['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue',


'def', 'del', 'elif', 'else', 'except', 'finally', 'for',
'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or',
'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

They are called keywords or (more precisely) reserved keywords. They are reserved
because you mustn't use them as names: neither for your variables, nor functions,
nor any other named entities you want to create.

The meaning of the reserved word is predefined, and mustn't be changed in any way.

Fortunately, due to the fact that Python is case-sensitive, you can modify any of
these words by changing the case of any letter, thus creating a new word, which is
not reserved anymore.

For example - you can't name your variable like this:

import

You mustn't have a variable named in such a way - it is prohibited. But you can do
this instead:

Import

the result of the input() function is a string.

Note: the range() function accepts only integers as its arguments, and generates
sequences of integers.

The loop's else branch is always executed once, regardless of whether the loop has
entered its body or not.

bitwise operators.
Here are all of them:

& (ampersand) - bitwise conjunction;


| (bar) - bitwise disjunction;
~ (tilde) - bitwise negation;
^ (caret) - bitwise exclusive or (xor).

Priority Operator
1 ~, +, - unary
2 **
3 *, /, //, %
4 +, - binary
5 <<, >>
6 <, <=, >, >=
7 ==, !=
8 &
9 |
10 =, +=, -=, *=, /=, %=, &=, ^=, |=, >>=, <<=

& does a bitwise and, e.g., x & y = 0, which is 0000 0000 in binary,
| does a bitwise or, e.g., x | y = 31, which is 0001 1111 in binary,
˜ does a bitwise not, e.g., ˜ x = 240*, which is 1111 0000 in binary,
^ does a bitwise xor, e.g., x ^ y = 31, which is 0001 1111 in binary,
>> does a bitwise right shift, e.g., y >> 1 = 8, which is 0000 1000 in binary,
<< does a bitwise left shift, e.g., y << 3 = , which is 1000 0000 in binary,

List is a collection of elements, but each element is a scalar.

the name of an ordinary variable is the name of its content;


the name of a list is the name of a memory location where the list is stored.

Take a look at the snippet:


my_list = [10, 8, 6, 4, 2]
new_list = my_list[1:3]
print(new_list)

The new_list list will have end - start (3 - 1 = 2) elements - the ones with
indices equal to 1 and 2 (but not 3).

my_list = [10, 8, 6, 4, 2]
del my_list
print(my_list)

The del instruction will delete the list itself, not its content.

Three-dimensional arrays
rooms = [[[False for r in range(20)] for f in range(15)] for t in range(3)]

In Python, the else block after a for or while loop runs only if the loop completes
without encountering a break.

Think of it like:
“Did the loop finish cleanly? If yes → run the else block.”

f[start:stop]
It returns a new list that includes elements from index start up to but not
including index stop.

k=[[1,2],[3,4]]
print(k[0][0])
1
print(k[0][1])
2
print(k[1][1])
4
print(k[1][0])
3

The or operator in Python works like this:

It returns True if either operand is True

It returns False only if both are False

list.insert(index, element)
s=[1,2,3]
for v in range(len(s)):
s.insert(1,s[v])
print(s)

You mustn't have a function and a variable of the same name.

Mixing positional and keyword arguments


You can mix both fashions if you want - there is only one unbreakable rule: you
have to put positional arguments before keyword arguments.

It's important to remember that positional arguments mustn't follow keyword


arguments. That's why if you try to run the following snippet:

positional argument passing in which the order of arguments passed matters (Ex. 1),
keyword (named) argument passing in which the order of arguments passed doesn't
matter

may a list be a function result?

Yes, of course! Any entity recognizable by Python can be a function result.

def my_function():
#global var
var = 2
print("Do I know that variable?", var)

global var
var = 1
my_function()
print(var)

=Do I know that variable? 2


=1

The conclusion is obvious - changing the parameter's value doesn't propagate


outside the function (in any case, not when the variable is a scalar, like in the
example).

This also means that a function receives the argument's value, not the argument
itself. This is true for scalars.

a sequence is data which can be scanned by the for loop.

Mutable data can be freely updated at any time. Immutable data cannot be modified
in this way.

A tuple is an immutable sequence type. It can behave like a list, but it mustn't be
modified. Each tuple element may be of a different type (floating-point, integer,
or any other not-as-yet-introduced kind of data).

dictionaries are not lists - they don't preserve the order of their data, as the
order is completely meaningless (unlike in real, paper dictionaries). The order in
which a dictionary stores its data is completely out of your control, and your
expectations. That's normal. (*)(*) In Python 3.6x dictionaries have become ordered
collections by default. Your results may vary depending on what Python version
you're using.

keys are case-sensitive:

And now the most important news: you mustn't use a non-existent key.

1. Tuples are ordered and unchangeable (immutable) collections of data. They can be
thought of as immutable lists. They are written in round brackets:

1. Dictionaries are unordered*, changeable (mutable), and indexed collections of


data. (*In Python 3.6x dictionaries have become ordered by default.

The default except branch must be the last except branch. Always!

d={'a':'a11','b':'b11','c':'c11'}
v=d['a']
print(v)
#for i in range(len(d)):
# v=d[v]

#print(v)

ls=['a','b','c','d','e']

def l(l1):
del l1[3]
l1[3]='e'

print(l(ls))

dd={'1':'0','0':'1','2':'3'}
v=dd['2']
v=dd['4']
print(v)
print(len(dd))

from math import sin, pi

print(sin(pi/2))
def sin(x):
if 2 * x == pi:
return 0.99999999
else:
return None

print(sin(pi/2))

Note: after successful execution of an aliased import, the original module name
becomes inaccessible and must not be used.

dir(). The function returns an alphabetically sorted list containing all entities'
names available in the module identified by a name passed to the function as an
argument:

When a module is imported, its content is implicitly executed by Python.

During the very first import of the actual module, Python translates its source
code into the semi-compiled format stored inside the pyc files, and deploys these
files into the __pycache__ directory located in the module's home directory.

A Python file named __init__.py is implicitly run when a package containing it is


subject to import, and is used to initialize a package and/or its sub-packages (if
any). The file may be empty, but must not be absent.

Centralized repository of all available software packages; and a tool allowing


users to access the repository. Both these entities already exist and can be used
at any time.

The repository (or repo for short) we mentioned before is named PyPI (it's short
for Python Package Index) and it's maintained by a workgroup named the Packaging
Working Group, a part of the Python Software Foundation, whose main task is to
support Python developers in efficient code dissemination.

“all Python roads lead to PyPl”. PyPI is completely free, and you can just pick a
code and use it.

Is every user doomed to visit hell in order to run the code for the first time?
Fortunately not - pip can do all of this for you. Really. It can discover,
identify, and resolve all dependencies. Moreover, it can do it in the cleverest
way, avoiding any unnecessary downloads and reinstalls.

print("tau tau tau".rfind("ta"))


print("tau tau tau".rfind("ta", 9))
print("tau tau tau".rfind("ta", 3, 9))

8
-1
4
1. Some of the methods offered by strings are:

capitalize() – changes all string letters to capitals;


center() – centers the string inside the field of a known length;
count() – counts the occurrences of a given character;
join() – joins all items of a tuple/list into one string;
lower() – converts all the string's letters into lower-case letters;
lstrip() – removes the white characters from the beginning of the string;
replace() – replaces a given substring with another;
rfind() – finds a substring starting from the end of the string;
rstrip() – removes the trailing white spaces from the end of the string;
split() – splits the string into a substring using a given delimiter;
strip() – removes the leading and trailing white spaces;
swapcase() – swaps the letters' cases (lower to upper and vice versa)
title() – makes the first letter in each word upper-case;
upper() – converts all the string's letter into upper-case letters.

2. String content can be determined using the following methods (all of them return
Boolean values):

endswith() – does the string end with a given substring?


isalnum() – does the string consist only of letters and digits?
isalpha() – does the string consist only of letters?
islower() – does the string consists only of lower-case letters?
isspace() – does the string consists only of white spaces?
isupper() – does the string consists only of upper-case letters?
startswith() – does the string begin with a given substring?

Take a look at the figure:

Tree-shaped hierarchy of selected exceptions

It shows a small section of the complete exception tree. Let's begin examining the
tree from the ZeroDivisionError leaf.

BaseException

Exception

ArithmeticError

ZeroDivisionError

https://docs.python.org/3.6/library/exceptions.html

-------------------------------------------------------------------------------
------------------------------------OOP----------------------------------------
-------------------------------------------------------------------------------
if you set a property's value for the very first time (like in the constructor),
you are creating it; from that moment on, the object has got the property and is
ready to use its value;

When any class component has a name starting with two underscores (__), it becomes
private - this means that it can be accessed only from within the class.

You cannot see it from the outside world. This is how Python implements the
encapsulation concept.

Python objects, when created, are gifted with a small set of predefined properties
and methods. Each object has got them, whether you want them or not. One of them is
a variable named __dict__ (it's a dictionary).

class ExampleClass:
varia = 1
def __init__(self, val):
ExampleClass.varia = val
varia = val
self.varia = val

The self parameter is used to obtain access to the object's instance and class
variables.

The self parameter is also used to invoke other object/class methods from inside
the class.

Note: the __name__ attribute is absent from the object - it exists only inside
classes.

__bases__ is a tuple. The tuple contains classes (not class names) which are direct
superclasses for the class.

Note: only classes have this attribute - objects don't.

Key takeaways

1. A method is a function embedded inside a class. The first (or only) parameter of
each method is usually named self, which is designed to identify the object for
which the method is invoked in order to access the object's properties or invoke
its methods.

2. If a class contains a constructor (a method named __init__) it cannot return any


value and cannot be invoked directly.

3. All classes (but not objects) contain a property named __name__, which stores
the name of the class. Additionally, a property named __module__ stores the name of
the module in which the class has been declared, while the property named __bases__
is a tuple containing a class's superclasses.
super() function, which accesses the superclass without needing to know its name:

class Level1:
variable_1 = 100
def __init__(self):
self.var_1 = 101

def fun_1(self):
return 102

class Level2(Level1):
variable_2 = 200
def __init__(self):
super().__init__()
self.var_2 = 201

def fun_2(self):
return 202

class Level3(Level2):
variable_3 = 300
def __init__(self):
super().__init__()
self.var_3 = 301

def fun_3(self):
return 302

obj = Level3()

print(obj.variable_1, obj.var_1, obj.fun_1())


print(obj.variable_2, obj.var_2, obj.fun_2())
print(obj.variable_3, obj.var_3, obj.fun_3())
100 101 102
200 201 202
300 301 302

Note: the situation in which the subclass is able to modify its superclass behavior
(just like in the example) is called polymorphism.

===================================================================================
=============================================================
==OOP==============================================================================
=============================================================
===================================================================================
=============================================================

Key takeaways

1. A method named __str__() is responsible for converting an object's contents into


a (more or less) readable string. You can redefine it if you want your object to be
able to present itself in a more elegant form. For example:

class Mouse:
def __init__(self, name):
self.my_name = name

def __str__(self):
return self.my_name

the_mouse = Mouse('mickey')
print(the_mouse) # Prints "mickey".

2. A function named issubclass(Class_1, Class_2) is able to determine if Class_1 is


a subclass of Class_2. For example:

class Mouse:
pass

class LabMouse(Mouse):
pass

print(issubclass(Mouse, LabMouse), issubclass(LabMouse, Mouse)) # Prints "False


True"

3. A function named isinstance(Object, Class) checks if an object comes from an


indicated class. For example:

class Mouse:
pass

class LabMouse(Mouse):
pass

mickey = Mouse()
print(isinstance(mickey, Mouse), isinstance(mickey, LabMouse)) # Prints "True
False".

4. A operator called is checks if two variables refer to the same object. For
example:

class Mouse:
pass

mickey = Mouse()
minnie = Mouse()
cloned_mickey = mickey
print(mickey is minnie, mickey is cloned_mickey) # Prints "False True".
5. A parameterless function named super() returns a reference to the nearest
superclass of the class. For example:

class Mouse:
def __str__(self):
return "Mouse"

class LabMouse(Mouse):
def __str__(self):
return "Laboratory " + super().__str__()

doctor_mouse = LabMouse();
print(doctor_mouse) # Prints "Laboratory Mouse".

6. Methods as well as instance and class variables defined in a superclass are


automatically inherited by their subclasses. For example:

class Mouse:
Population = 0
def __init__(self, name):
Mouse.Population += 1
self.name = name

def __str__(self):
return "Hi, my name is " + self.name

class LabMouse(Mouse):
pass

professor_mouse = LabMouse("Professor Mouser")


print(professor_mouse, Mouse.Population) # Prints "Hi, my name is Professor Mouser
1"

7. In order to find any object/class property, Python looks for it inside:

the object itself;


all classes involved in the object's inheritance line from bottom to top;
if there is more than one class on a particular inheritance path, Python scans them
from left to right;
if both of the above fail, the AttributeError exception is raised.

8. If any of the subclasses defines a method/class variable/instance variable of


the same name as existing in the superclass, the new name overrides any of the
previous instances of the name. For example:

class Mouse:
def __init__(self, name):
self.name = name

def __str__(self):
return "My name is " + self.name

class AncientMouse(Mouse):
def __str__(self):
return "Meum nomen est " + self.name

mus = AncientMouse("Caesar") # Prints "Meum nomen est Caesar"


print(mus)

A variable existing outside a function has a scope inside the functions' bodies.

If the argument is a list, then changing the value of the corresponding parameter
doesn't affect the list (remember: variables containing lists are stored in a
different way than scalars),
but if you change a list identified by the parameter (note: the list, not the
parameter!), the list will reflect the change.

A sequence is data which can be scanned by the for loop.

A tuple is an immutable sequence type.

+The range() function is, in fact, a generator, which is (in fact, again) an
iterator++

Generators - where to find them: continued


The iterator protocol is a way in which an object should behave to conform to the
rules imposed by the context of the for and in statements. An object conforming to
the iterator protocol is called an iterator.

An iterator must provide two methods:

__iter__() which should return the object itself and which is invoked once (it's
needed for Python to successfully start the iteration)
__next__() which is intended to return the next value (first, second, and so on) of
the desired series - it will be invoked by the for/in statements in order to pass
through the next iteration; if there are no more values to provide, the method
should raise the StopIteration exception.
Does it sound strange? Not at all. Look at the example in the editor.

We've built a class able to iterate through the first n values (where n is a
constructor parameter) of the Fibonacci numbers.

Let us remind you - the Fibonacci numbers (Fibi) are defined as follows:

Fib1 = 1
Fib2 = 1
Fibi = Fibi-1 + Fibi-2

In other words:

the first two Fibonacci numbers are equal to 1;


any other Fibonacci number is the sum of the two previous ones (e.g., Fib3 = 2,
Fib4 = 3, Fib5 = 5, and so on)
Let's dive into the code:

lines 2 through 6: the class constructor prints a message (we'll use this to trace
the class's behavior), prepares some variables (__n to store the series limit, __i
to track the current Fibonacci number to provide, and __p1 along with __p2 to save
the two previous numbers);

lines 8 through 10: the __iter__ method is obliged to return the iterator object
itself; its purpose may be a bit ambiguous here, but there's no mystery; try to
imagine an object which is not an iterator (e.g., it's a collection of some
entities), but one of its components is an iterator able to scan the collection;
the __iter__ method should extract the iterator and entrust it with the execution
of the iteration protocol; as you can see, the method starts its action by printing
a message;

lines 12 through 21: the __next__ method is responsible for creating the sequence;
it's somewhat wordy, but this should make it more readable; first, it prints a
message, then it updates the number of desired values, and if it reaches the end of
the sequence, the method breaks the iteration by raising the StopIteration
exception; the rest of the code is simple, and it precisely reflects the definition
we showed you earlier;

lines 24 and 25 make use of the iterator.

The code produces the following output:

__init__
__iter__
__next__
1
__next__
1
__next__
2
__next__
3
__next__
5
__next__
8
__next__
13
__next__
21
__next__
34
__next__
55
__next__
output

Look:

the iterator object is instantiated first;


next, Python invokes the __iter__ method to get access to the actual iterator;
the __next__ method is invoked eleven times - the first ten times produce useful
values, while the eleventh terminates the iteration.

conditional expression - a way of selecting one of two different values based on


the result of a Boolean expression.

The map() function applies the function passed by its first argument to all its
second argument's elements, and returns an iterator delivering all subsequent
function results.
closure is a technique which allows the storing of values in spite of the fact that
the context in which they have been created does not exist anymore.

1. An iterator is an object of a class providing at least two methods (not counting


the constructor):

__iter__() is invoked once when the iterator is created and returns the iterator's
object itself;
__next__() is invoked to provide the next iteration's value and raises the
StopIteration exception when the iteration comes to an end.

2. The yield statement can be used only inside functions. The yield statement
suspends function execution and causes the function to return the yield's argument
as a result. Such a function cannot be invoked in a regular way – its only purpose
is to be used as a generator (i.e. in a context that requires a series of values,
like a for loop.)

3. A conditional expression is an expression built using the if-else operator. For


example:

print(True if 0 >= 0 else False)

outputs True.

4. A list comprehension becomes a generator when used inside parentheses (used


inside brackets, it produces a regular list). For example:

for x in (el * 2 for el in range(5)):


print(x)

outputs 02468.

5. A lambda function is a tool for creating anonymous functions. For example:

def foo(x, f):


return f(x)

print(foo(9, lambda x: x ** 0.5))

outputs 3.0.

6. The map(fun, list) function creates a copy of a list argument, and applies the
fun function to all of its elements, returning a generator that provides the new
list content element by element. For example:

short_list = ['mython', 'python', 'fell', 'on', 'the', 'floor']


new_list = list(map(lambda s: s.title(), short_list))
print(new_list)

outputs ['Mython', 'Python', 'Fell', 'On', 'The', 'Floor'].


7. The filter(fun, list) function creates a copy of those list elements, which
cause the fun function to return True. The function's result is a generator
providing the new list content element by element. For example:

short_list = [1, "Python", -1, "Monty"]


new_list = list(filter(lambda s: isinstance(s, str), short_list))
print(new_list)

outputs ['Python', 'Monty'].

8. A closure is a technique which allows the storing of values in spite of the fact
that the context in which they have been created does not exist anymore. For
example:

def tag(tg):
tg2 = tg
tg2 = tg[0] + '/' + tg[1:]

def inner(str):
return tg + str + tg2
return inner

b_tag = tag('<b>')
print(b_tag('Monty Python'))

outputs <b>Monty Python</b>

1. A file needs to be open before it can be processed by a program, and it should


be closed when the processing is finished.

Opening the file associates it with the stream, which is an abstract representation
of the physical data stored on the media. The way in which the stream is processed
is called open mode. Three open modes exist:

read mode – only read operations are allowed;


write mode – only write operations are allowed;
update mode – both writes and reads are allowed.

2. Depending on the physical file content, different Python classes can be used to
process files. In general, the BufferedIOBase is able to process any file, while
TextIOBase is a specialized class dedicated to processing text files (i.e. files
containing human-visible texts divided into lines using new-line markers). Thus,
the streams can be divided into binary and text ones.

3. The following open() function syntax is used to open a file:

open(file_name, mode=open_mode, encoding=text_encoding)

The invocation creates a stream object and associates it with the file named
file_name, using the specified open_mode and setting the specified text_encoding,
or it raises an exception in the case of an error.
4. Three predefined streams are already open when the program starts:

sys.stdin – standard input;


sys.stdout – standard output;
sys.stderr – standard error output.

5. The IOError exception object, created when any file operations fails (including
open operations), contains a property named errno, which contains the completion
code of the failed action. Use this value to diagnose the problem.

Key takeaways

1. To read a file’s contents, the following stream methods can be used:

read(number) – reads the number characters/bytes from the file and returns them as
a string; is able to read the whole file at once;
readline() – reads a single line from the text file;
readlines(number) – reads the number lines from the text file; is able to read all
lines at once;
readinto(bytearray) – reads the bytes from the file and fills the bytearray with
them;

2. To write new content into a file, the following stream methods can be used:

write(string) – writes a string to a text file;


write(bytearray) – writes all the bytes of bytearray to a file;

3. The open() method returns an iterable object which can be used to iterate
through all the file's lines inside a for loop. For example:

for line in open("file", "rt"):


print(line, end='')

The code copies the file's contents to the console, line by line. Note: the stream
closes itself automatically when it reaches the end of the file.

YAML vs Other Formats


Feature YAML JSON XML
Syntax Indentation-based Bracket-based Tag-based
Readability Very human-friendly Less human-friendly Verbose
Comments ✅ Yes (#) 🚫 No native support ✅ Yes
Used in Configs, CI/CD, DevOps APIs, data exchange Legacy systems, HTML
data

Module 4:

generators and iterators;


list comprehensions;
the lambda, map, and filter functions;
closures;
working with files (file streams, file processing, diagnosing stream problems)
processing text and binary files;
selected Python STL modules: os, datetime, time, and calendar.

You are now ready to take the module quiz and attempt the final challenge: Module 4
Test, which will help you gauge what you've learned so far.

===================================================================================
===================================================================================
==
Could you explain Objects __dict__ vs Class's __dict__ in detail with example and
differences?
===================================================================================
===================================================================================
==

Great question! This is one of those Python details that confuses even experienced
developers at first. Let’s break it down carefully.

---

## 1. What is `__dict__`?

* In Python, every object can store attributes (variables bound to it).


* These attributes are usually stored in a **dictionary-like structure** called
`__dict__`.
* Both **instances (objects)** and **classes** can have a `__dict__`.

But they behave differently:

---

## 2. `object.__dict__` (Instance dictionary)

* For an **instance of a class**, `__dict__` holds **the attributes assigned to


that specific object**.
* It does **not** automatically include attributes defined at the class level —
unless they are overridden by the instance.

👉 Example:

```python
class Car:
wheels = 4 # class attribute

def __init__(self, color):


self.color = color # instance attribute

# Create two Car objects


c1 = Car("red")
c2 = Car("blue")

print(c1.__dict__)
print(c2.__dict__)
```
**Output:**

```python
{'color': 'red'}
{'color': 'blue'}
```

Notice:

* `wheels` does not appear in the instance `__dict__` because it belongs to the
**class**, not the specific instance.
* Each object only stores what was set via `self` (its unique state).

---

## 3. `Class.__dict__` (Class dictionary)

* For a **class itself**, `__dict__` is a special **mapping proxy** that shows:

* Class-level attributes (`wheels`)


* Methods (`__init__`)
* Other metadata (`__module__`, `__doc__`, etc.)

👉 Example (continuing):

```python
print(Car.__dict__)
```

**Output (simplified):**

```python
{
'__module__': '__main__',
'wheels': 4,
'__init__': <function Car.__init__ at 0x...>,
'__dict__': <attribute '__dict__' of 'Car' objects>,
'__weakref__': <attribute '__weakref__' of 'Car' objects>,
'__doc__': None
}
```

Notice:

* The class dictionary contains `wheels` and the method `__init__`.


* It does **not** contain `c1.color` or `c2.color` because those are **instance-
specific**.

---

## 4. Key Differences

| Aspect | `object.__dict__` (Instance) |


`Class.__dict__` |
| --------------- | ------------------------------------------------- |
------------------------------------------- |
| What it stores | Attributes of that instance only (set via `self`) | Class
attributes, methods, metadata |
| Scope | Per-object (unique to each instance) | Shared
across all instances |
| Type | Regular `dict` |
`mappingproxy` (read-only view of dict) |
| Example content | `{'color': 'red'}` | `{'wheels':
4, '__init__': <function ...>}` |

---

## 5. Demonstrating Interaction

```python
class Person:
species = "Homo sapiens"

def __init__(self, name):


self.name = name

p = Person("Alice")

print("Instance dict:", p.__dict__)


print("Class dict keys:", list(Person.__dict__.keys()))

# Accessing class attribute from instance


print(p.species) # "Homo sapiens"

# Overriding class attribute at instance level


p.species = "Alien"
print("Instance dict after override:", p.__dict__)
print("p.species ->", p.species)
print("Person.species ->", Person.species)
```

**Output:**

```python
Instance dict: {'name': 'Alice'}
Class dict keys: ['__module__', 'species', '__init__', '__dict__', '__weakref__',
'__doc__']
Homo sapiens
Instance dict after override: {'name': 'Alice', 'species': 'Alien'}
p.species -> Alien
Person.species -> Homo sapiens
```

Explanation:

* Initially, `species` comes from the class dictionary.


* Once we assign `p.species = "Alien"`, that key/value gets added to the **instance
`__dict__`** and shadows the class attribute.

---

✅ **In short:**

* `object.__dict__` = *per-instance storage of attributes*


* `Class.__dict__` = *class-wide storage of attributes & methods*

---
===================================================================================
===================================================================================
======
Great question 👍 — **name mangling** in Python often shows up when working with
**double underscores** in class attributes, and it can be confusing at first. Let’s
go step by step.
===================================================================================
===================================================================================
======

---

## 🔹 1. What is Name Mangling?

* **Name mangling** is the process by which Python **changes the name** of a class
attribute that starts with **two leading underscores (`__`)** (but does **not** end
with double underscores).
* Python does this to **prevent accidental access or overriding** of such
attributes in subclasses.

👉 Basically:
`__myvar` inside class `MyClass` becomes `_MyClass__myvar` internally.

---

## 🔹 2. Why Does Python Do This?

* To avoid **attribute collisions** when subclasses are created.


* It makes the attribute harder to access from outside, though not truly private
(Python doesn’t have strict private members like Java or C++).

---

## 🔹 3. Example: Name Mangling in Action

```python
class Test:
def __init__(self):
self.__secret = "hidden"
self.visible = "public"

t = Test()

print(t.visible) # ✅ works fine


# print(t.__secret) # ❌ AttributeError

# But internally, Python stored it like this:


print(t._Test__secret) # ✅ 'hidden'
```

**Output:**

```
public
hidden
```

Notice:

* `__secret` became `_Test__secret`.


* That’s name mangling at work.

---

## 🔹 4. How It Protects Against Subclass Collisions

```python
class Parent:
def __init__(self):
self.__data = 42 # becomes _Parent__data

class Child(Parent):
def __init__(self):
super().__init__()
self.__data = 99 # becomes _Child__data

c = Child()
print(c.__dict__)
```

**Output:**

```python
{'_Parent__data': 42, '_Child__data': 99}
```

👉 See? The parent’s `__data` and the child’s `__data` **don’t clash**.
That’s the real purpose of name mangling.

---

## 🔹 5. Important Notes

1. **Only applies** when you use `__var` (double leading underscore) inside a
class.

* `__var__` (double leading and trailing underscores) is reserved for Python’s


special methods (`__init__`, `__str__`, etc.) and **does not get mangled**.
* `_var` (single underscore) is just a **convention** meaning “protected”, but
Python doesn’t enforce it.

2. Name mangling is not true privacy — you can still access the variable if you
know the mangled name (`_ClassName__var`).

---

✅ **In short:**

* **Name mangling** = Python automatically renames attributes like `__x` →


`_ClassName__x` inside a class.
* It’s meant to avoid accidental name clashes in subclasses, not to enforce strict
privacy.
---

Would you like me to also show you **real-world best practices** (when to use
`_var` vs `__var` vs `__var__`)? That often clears up when name mangling is
actually useful.

🔹 Example 1: Simple division error


try:
x = 10 / 0
except Exception as exception:
print("args:", exception.args)
print("type:", type(exception))

Output:

args: ('division by zero',)


type: <class 'ZeroDivisionError'>

🔹 Example 2: Custom raise


try:
raise ValueError("Invalid input!", 404)
except Exception as exception:
print(exception.args)

Output:

('Invalid input!', 404)

When using import a.b, Python expects:

a to be a package (a directory with __init__.py),

and b to be either a submodule (a .py file) or a subpackage (a subdirectory of a


with its own __init__.py).

So yes, b can itself be a package, but only if it is a subpackage of a.

You cannot use import a.b to import a class, function, or variable—only modules or
packages may be imported this way

⚡ Summary of all possible permutations

import module

import module as alias

import module1, module2


import module1 as a1, module2 as a2

from module import object

from module import object1, object2

from module import object as alias

from module import object1 as a1, object2 as a2

from module import *

import package.submodule

import package.submodule as alias

from package.submodule import object

| Object type | Example import | Usage |


| ------------ | ------------------------------- | ----------------------- |
| Function | `from math import sqrt` | `sqrt(9)` |
| Class | `from datetime import datetime` | `datetime.now()` |
| Constant | `from math import pi` | `pi` |
| Variable | `from config import settings` | `settings["debug"]` |
| Submodule | `from os import path` | `path.join()` |
| Module | `import math` | `math.sqrt(9)` |
| Package | `import xml` | `xml.etree.ElementTree` |
| Special attr | `from math import __doc__` | `print(__doc__)` |

1. Use dir(module)

The built-in dir() function lists all attributes (functions, classes, constants,
variables, submodules, etc.) of a module.

2. Use help(module)

3. Use module.__all__ (if defined)

Some modules define a special list __all__.


That’s the official list of public names that from module import * would import.

6. Use pydoc

From the terminal:

python -m pydoc math

✅ So, in short:

All objects: dir(module)

Recommended imports: module.__all__ (if available)


Documentation: help(module) or pydoc

Categorization: inspect

When Python first imports a module, it compiles the module into bytecode and saves
the compiled bytecode in a file with a .pyc extension. The .pyc file contains the
bytecode of the module and is used to speed up subsequent imports of the same
module.

The location where Python deploys the .pyc files depends on the setting of the
__pycache__ directory. The __pycache__ directory is a directory where Python stores
the compiled bytecode files.

The location of the __pycache__ directory depends on the value of the


sys.pycache_prefix variable, which is set to a default value based on the operating
system and the user's environment. The sys.pycache_prefix variable specifies the
root directory where Python should store the __pycache__ directory.

In general, the __pycache__ directory is located in the same directory as the


source code file or in a __pycache__ subdirectory of the directory where the source
code file is located. The name of the .pyc file is the same as the name of the
source code file, with the extension changed to .cpython-<version>.pyc, where
<version> is the version number of the Python interpreter used to compile the
bytecode.

For example, if you have a module called my_module.py and you import it for the
first time, Python will compile it into bytecode and save the compiled bytecode in
a file called my_module.cpython-39.pyc. If the __pycache__ directory is located in
the same directory as the source code file, then the .pyc file will be stored in
the same directory. If the __pycache__ directory is located in a subdirectory of
the source code directory, then the .pyc file will be stored in that subdirectory.

You might also like