[Go to site: main page, start]

0% found this document useful (0 votes)
4 views17 pages

Mod 5 Notes Python

The document discusses object-oriented programming (OOP) concepts using a Rectangle class as an example, emphasizing the importance of attributes, composition, and methods for managing object state. It also covers the distinctions between shallow and deep copies, equality checks, and the MyTime class for time representation, highlighting the differences between pure functions and modifiers. Key programming practices such as encapsulation, reusability, and clarity in code are also addressed.

Uploaded by

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

Mod 5 Notes Python

The document discusses object-oriented programming (OOP) concepts using a Rectangle class as an example, emphasizing the importance of attributes, composition, and methods for managing object state. It also covers the distinctions between shallow and deep copies, equality checks, and the MyTime class for time representation, highlighting the differences between pure functions and modifiers. Key programming practices such as encapsulation, reusability, and clarity in code are also addressed.

Uploaded by

madhu010p
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

1BPLC105B Python Programming

Module 5
Chapter 1- Object oriented programming

Defining a Rectangle Object


When designing a class, the first step is deciding which attributes are necessary to represent the concept
accurately. For a rectangle in a 2D plane (not at an angle), we need both location and size.
Design Choice: While there are many ways to define a rectangle (center point, two opposite
corners, etc.), the conventional choice in programming is to store the upper-left corner and the
dimensions (width and height).

Object Composition (Objects within Objects)


One of the most powerful features of OOP is composition. Instead of storing the corner as two separate
numbers (x and y), we can use an instance of the Point class we previously created.

The Rectangle Class Definition


class Rectangle:
def init (self, posn, w, h):
""" Initialize rectangle at posn (a Point), with width w, height h """
[Link] = posn # This is a Point object
[Link] = w
[Link] = h
def str (self):
return "({0}, {1}, {2})".format([Link], [Link], [Link])

Why use Composition?


Reusability: We don't have to rewrite the logic for coordinates; we simply reuse the Point class.
Logical Hierarchy: It makes sense that a Rectangle "has a" Point as its corner.

Navigating Composed Objects (The Dot Operator)


When objects are nested inside other objects, the dot operator allows you to "drill down" into the data
levels. This is called composition of expressions.
Expression: [Link].x
Translation:
Start at box (the Rectangle).
Go to the corner attribute (the Point).
Select the x attribute from that Point.

Practical Implementation
When we instantiate a Rectangle, we pass a Point instance as the first argument:
# Create a Rectangle starting at (0,0) with width 100 and height 200
box = Rectangle(Point(0, 0), 100, 200)
# Create a smaller Rectangle at (100, 80)
bomb = Rectangle(Point(100, 80), 5, 10)

Output Analysis
When we print(box), Python triggers the str method. Because the corner is a Point object, Python
also automatically triggers the Point class's str method to display (0, 0).
Result: box: ((0, 0), 100, 200)
1BPLC105B Python Programming

Summary Table
Level Attribute Type Example

Top Level box Rectangle The whole shape

Nested Level [Link] Point The upper-left coordinate

Data Level [Link].x float/int The specific X value

The figure shows the state of this object:

Objects are mutable


Changing Object State
Because instances of user-defined classes are mutable, we can update their internal data after they have
been created. This can be done in two ways:

Direct Attribute Assignment


You can modify attributes directly using dot notation.
Example: [Link] += 50
This is a "quick" way to change state, but it is often better to use methods to keep the
logic organized.

Encapsulation via Methods


A better practice is to create methods that handle the changes. This allows the object to "manage" its
own transformation.

Implementing Transformation Methods


The Rectangle class can be expanded with methods to change its size and location.

The grow Method


This method modifies the dimensions of the rectangle relative to its current size.
Example:
def grow(self, delta_width, delta_height):
[Link] += delta_width
[Link] += delta_height
Positive deltas: Increase the size (grow).
Negative deltas: Decrease the size (shrink).

The move Method


This method changes the position of the rectangle by updating the nested Point object.
Example:
def move(self, dx, dy):
[Link].x += dx
[Link].y += dy
1BPLC105B Python Programming

This highlights the composition we discussed earlier: to move the rectangle, we actually modify
the x and y attributes of the Point instance stored in [Link].

Practical Example: Step-by-Step Mutation


Action Code Resulting State

Initial r = Rectangle(Point(10,5), 100, 50) ((10, 5), 100, 50)

Grow/Shrink [Link](25, -10) ((10, 5), 125, 40)

Move [Link](-10, 10) ((0, 15), 125, 40)

Key Concept: Side Effects


When you call a method like [Link](), the object is modified in-place.
a) The method does not necessarily need to return anything because the "work" is done directly
on the memory address referenced by self.
b) If other variables are aliased to r, they will see these changes immediately.

Sameness
The meaning of the word “same” seems perfectly clear until we give it some thought, and then we
realize there is more to it than we initially expected.

For example, if we say, “Alice and Bob have the same car”, we mean that her car and his are the same
make and model, but that they are two different cars. If we say, “Alice and Bob have the same mother”,
we mean that her mother and his are the same person.

That analogy of the "Same Car" versus the "Same Mother" is one of the most effective ways to
understand how Python manages memory. It distinguishes between identical values and identical
identities.

Shallow Equality (Identity)


Shallow equality asks: "Are these two variables pointing to the exact same spot in memory?"
The is Operator: This operator checks for identity. If p1 is p2 is True, they are aliases for a
single object.
Memory References: Even if two objects have identical data (like two different cars of the
same make), they are distinct entities in memory.

Deep Equality (Value)


Deep equality asks: "Do these two objects contain the same data, regardless of where they are stored?"
Custom Functions: For user-defined classes like Point, you often have to write your own logic
to check for deep equality.
Example:
def same_coordinates(p1, p2):
return (p1.x == p2.x) and (p1.y == p2.y)
Logical Consistency: If two objects have shallow equality (identity), they automatically have
deep equality because they are literally the same thing.

Testing Sameness in Python


Using the Point class example, here is how the behavior looks in code:
1BPLC105B Python Programming

Case A: Different Objects, Same Data


Example:
p1 = Point(3, 4)
p2 = Point(3, 4)
print(p1 is p2) # False (They are different "cars")

Case B: Aliasing (The Same Object)


Example:
p3 = p1
print(p1 is p3) # True (They point to the same "mother")

The Ambiguity of ==
In Python, the == operator's behavior depends entirely on how the class was designed. This is a concept
known as operator overloading.

Comparison of Default Behaviors


== Result for Same
Datatype Type of Equality
Data

User-defined Shallow: By default, Python assumes new classes only


False
(Points) equal themselves.

Built-in Deep: Python's developers designed lists to compare


True
(Lists/Tuples) their contents.

Why This Matters


As a programmer, you shouldn't assume what == does for an object you didn't create.
For Points: p1 == p2 will likely be False even if both are (3, 4).
For Lists: [1, 2] == [1, 2] will be True.
The "Humpty Dumpty" Rule: In Python, the person who writes the class gets to
decide what the operators mean. Later, you can learn to use the magic method eq to
make p1 == p2 behave like deep equality for your own classes.

Summary Table: is vs ==
Operator Question Asked Terminology

is Are you the same object? Identity / Shallow

== Do you have the same value? Value / Deep (Usually)


· is : Checks for Identity (Same memory address).
· == : Checks for Equality (Same values/content, but behavior is class-dependent).

Aliasing
The Problem with Aliasing
As previously discussed, aliasing happens when two variables point to the same object. While efficient
for memory, it is dangerous because:
Changes to one variable unexpectedly affect another.
It becomes difficult to track which parts of a program can modify a specific object.
1BPLC105B Python Programming

Shallow Copy ([Link])


A shallow copy creates a new object, but it does not create new copies of the objects inside it. Instead,
it just copies the references to those internal objects.
For Simple Objects: For a Point(x, y), a shallow copy is sufficient because x and y are simple
numbers.
For Compound Objects: For a Rectangle, a shallow copy creates a new Rectangle instance,
but both the old and new rectangles share the same Point object for their corner.
The "Move" Trap: If you have rect2 = [Link](rect1) and you call [Link](), the
corner of rect1 will also move because they share the same Point instance.

Deep Copy ([Link])


A deep copy is the solution for complex or nested objects. It copies the main object and recursively
copies every object found inside it.
Result: The new object is 100% independent of the original.
Safety: Modifying any part of the deep-copied object (even nested attributes like [Link].x)
will have no effect on the original object.

Summary: Shallow vs. Deep Copy


Feature [Link](obj) (Shallow) [Link](obj) (Deep)

New Top-Level
Yes Yes
Object?

New Nested Objects? No (References are shared) Yes (New instances created)

Simple objects (Point, Flat Compound objects (Rectangle, Nested


Best Use Case
lists) lists)

High (Potential for side


Risk Low (Total independence)
effects)

Code Example

import copy
# Shallow copy: b1 and b2 share the same corner Point
b2 = [Link](b1)
# Deep copy: b3 has its own unique corner Point
b3 = [Link](b1)

If we create a box, b1, in the usual way and then make a copy, b2, using copy, the resulting state
diagram looks like this:
1BPLC105B Python Programming

Chapter 2- Inheritance

MyTime Class
Defining a class like MyTime is a classic way to understand how objects store state (data) through
attributes.
In this example, the init method serves as the constructor. It’s the "blueprint instructions" for
what should happen the moment you create a new instance.
Default Arguments: By setting hrs=0, mins=0, secs=0, you make the parameters optional. If
you call MyTime(), it defaults to midnight (0:0:0).
Attributes: [Link], [Link], and [Link] are the internal variables that stay with the
object for its lifetime.

Visualizing the State


When you run tim1 = MyTime(11, 59, 30), Python allocates memory for a new object. A state diagram
represents how that data is organized within the instance.

Challenge: Adding the str Method


As your text mentioned, without a str method, printing tim1 would result in a messy, unhelpful
string like < main .MyTime object at 0x...>.
To make it readable (e.g., "11:59:30"), you can add this method to your class. Here is how you can
implement it using f-strings for clean formatting:

class MyTime:
def init (self, hrs=0, mins=0, secs=0):
[Link] = hrs
[Link] = mins
[Link] = secs

def str (self):


# The :02d ensures two digits with a leading zero if needed
return f"{[Link]:02d}:{[Link]:02d}:{[Link]:02d}"

# Testing it out:
tim1 = MyTime(11, 59, 30)
print(tim1) # Output: 11:59:30

The state diagram for the object looks like this:

Quick Tips for MyTime


Validation: Currently, someone could create MyTime(25, 70, 99). In a real-world scenario, you
might add logic to init to ensure hours are $< 24$ and minutes/seconds are $< 60$.
Initialization: Notice that the parameter names (hrs) don't have to match the attribute names
([Link]), though they often do for clarity.
1BPLC105B Python Programming

Pure functions
By defining add_time as a pure function, you ensure that your original objects (current_time and
bread_time) remain unchanged. This makes your code more predictable and easier to debug you don't
have to worry about a function "secretly" changing your data behind the scenes.

The "Carrying" Problem


As you noticed, the "better" version starts to address the overflow of seconds and minutes, but it still
has a logical flaw.

The Bug: What happens if s is 120?


In your current code, if s >= 60: only subtracts 60 once. If the sum of seconds is very large, you’ll still
end up with an invalid time (like 70 seconds).

Refactoring for Elegance


The text hints at an "alternative approach." Usually, in programming, when we deal with nested units
(like hours, minutes, seconds), it's much easier to convert everything to the smallest unit, perform the
math, and then convert back.
Think of it like money: instead of adding 2 dollars and $50$ cents to 1 dollar and 75 cents using
complex carries, you convert everything to cents (250 + 175 = 425) and then convert back to dollars
(4.25).

A Cleaner Approach (Functional Style)


Here is how that "better" code might look using integer division (//) and the modulo operator (%):

def add_time(t1, t2):


# 1. Convert everything to total seconds
total_seconds = ([Link] * 3600 + [Link] * 60 + [Link]) +
([Link] * 3600 + [Link] * 60 + [Link])
# 2. Use divmod to "unwrap" the units back into H, M, S
# Get total minutes and remaining seconds
minutes_temp, s = divmod(total_seconds, 60)
# Get hours and remaining minutes
h, m = divmod(minutes_temp, 60)
# 3. Handle 24-hour wrap-around if necessary
h %= 24
return MyTime(h, m, s)

Why this is better:


Robustness: It handles any number of seconds (even thousands) without multiple if statements.
Clarity: It follows a simple three-step logic: Flatten -> Calculate -> Reconstruct.
Purity: It still returns a brand new MyTime object, leaving t1 and t2 untouched.

Modifiers
While pure functions are like making a copy of a document and editing the copy, modifiers are like
editing the original document directly.
As you've noted, the increment function doesn't return anything. Instead, it reaches into the memory
address of the object t and updates its attributes. This is why it's called a side effect.

The Logic of the Modifier


Your use of while loops fixes the immediate bug where a large number of seconds (like 3600) would
have been ignored by a simple if statement.
1BPLC105B Python Programming

However, as the text suggests, this isn't "particularly good" code. Here is why:
Efficiency: If you pass 1,000,000 seconds, the while loop has to run over 16,000 times. That’s a
lot of unnecessary work for a computer.
Readability: It’s "clunky." We are manually doing carry-over math that Python's operators can
do in a single step.

Making the Modifier More "Pythonic"


We can use the same logic from the pure function—converting to a single unit—but apply the result
back to the original object t. This is much faster and cleaner.

def increment(t, seconds):


# 1. Total up all seconds currently in the object + the new seconds
total_seconds = [Link] * 3600 + [Link] * 60 + [Link] + seconds
# 2. Update the original object's attributes (The Modification)
# Using floor division and modulo
[Link], remaining_secs = divmod(total_seconds, 3600)
[Link], [Link] = divmod(remaining_secs, 60)
# 3. Optional: keep hours within a 24-hour clock
[Link] %= 24

Pure Function vs. Modifier: Which to use?


In modern programming, pure functions are generally preferred because they make code easier to
test and prevent "accidental" data changes. However, modifiers are useful when:
Memory is tight: You don't want to create thousands of new objects.
The "State" matters: You are specifically trying to track a single object that evolves over time
(like a character in a game).

Comparison Table
Feature Pure Function (add_time) Modifier (increment)

Return Value Returns a new object Usually returns None

Original Input Remains unchanged Is permanently altered

Predictability High (No side effects) Lower (Can change data elsewhere)

Converting increment to a method


Moving the function inside the class is the final step in encapsulation. By turning increment into a
method, you are bundling the data (hours, minutes, seconds) together with the behavior (how to add
time) that belongs to it.

The Mechanical Shift


As you noted, the transformation is straightforward. The key difference is the use of self. When you call
current_time.increment(500), Python effectively does this behind the scenes:
[Link](current_time, 500).

Why Methods are Preferred


In OOP, we want objects to be "smart" enough to manage themselves.
Discovery: If you have a MyTime object, you can type current_time. and your editor will show
you exactly what it can do (like increment).
1BPLC105B Python Programming

Organization: It keeps the global namespace clean. You don't have dozens of loose functions
floating around; they are tucked neatly inside the class they belong to.

Refining the Implementation


While the while loop version works, it’s a great time to apply the "convert to seconds" logic we
discussed earlier, but this time using the Method syntax. Here is how the complete, polished class
would look:

class MyTime:
def init (self, hrs=0, mins=0, secs=0):
[Link] = hrs
[Link] = mins
[Link] = secs
def str (self):
return f"{[Link]:02d}:{[Link]:02d}:{[Link]:02d}"
def increment(self, seconds):
# Convert the current state + increment to total seconds
total_seconds = [Link] * 3600 + [Link] * 60 + [Link] + seconds
# Re-calculate the attributes
[Link], remaining = divmod(total_seconds, 3600)
[Link], [Link] = divmod(remaining, 60)
# Keep it in a 24-hour format
[Link] %= 24

Pure Function or Method?


Notice that increment is still a modifier. It changes [Link], [Link], etc., in place. If you wanted
to keep the pure function philosophy but use method syntax, you would write a method that returns a
new object instead of modifying self:

def after(self, seconds):


"""Returns a NEW MyTime object, leaving the original unchanged."""
total = ([Link] * 3600 + [Link] * 60 + [Link]) + seconds
h, rem = divmod(total, 3600)
m, s = divmod(rem, 60)
return MyTime(h % 24, m, s)

An "Aha!" moment is the turning point where you stop thinking like a human manual-counter and
start thinking like a computer scientist. By recognizing that time is just a positional number system
(base 60), you reduce a complex logic problem into a simple math problem.

The Power of Normalization


The magic happens in the new init . By moving the "logic of time" into the constructor, you've
made the class self-healing. It no longer matters if the input is messy or "unnormalized"; the object
itself ensures the data is stored correctly.

Why this is a "Cleaner" Solution


Single Source of Truth: The logic for "how time works" lives in one place (the conversion
math).
Simplification of Operations: Look at how add_time changed. It went from nested if and while
loops to a single line of addition.
Consistency: Every time you create a MyTime object, you are guaranteed it will be in a valid
format (0 < min < 60 and 0 < sec < 60).
1BPLC105B Python Programming

Refactoring increment with the New Logic


Now that the init handles the heavy lifting, your increment method (which we previously wrote
with loops) becomes incredibly elegant. You don't even need to do the math inside increment; you just
let the constructor do it.

def increment(self, seconds):


"""Adds seconds to the current time using the new normalized init."""
# 1. Get total seconds currently in the object
curr_total = self.to_seconds()
# 2. Create a temporary normalized object
new_time = MyTime(0, 0, curr_total + seconds)
# 3. Update the current object's attributes (The Modifier part)
[Link] = new_time.hours
[Link] = new_time.minutes
[Link] = new_time.seconds

The Final Step: Operator Overloading


Since you've mastered the logic of adding time, Python has a special trick called operator overloading.
Instead of calling a function like add_time(t1, t2), you can tell Python that the + symbol should work
with your class by defining a method called add .

def add (self, other):


return MyTime(0, 0, self.to_seconds() + other.to_seconds())
# Now you can just do:
# t3 = t1 + t2

Generalization
The more you generalize the math, the more you can specialize the interface. By reducing MyTime
to an integer (total seconds), you move away from the "messy" human intuition of hours and minutes
and into the "clean" logic of mathematics. As you saw with the after method, the difference is
staggering—moving from a complex 13-line nested if structure to a single line of logic.

The Computer Scientist's "Best of Both Worlds"


The text explains that while a mathematician might just use an integer and call it a day, a computer
scientist uses that integer inside a specialized type.
This gives you:
The Mathematician's Simplicity: You use to_seconds() to make arithmetic and comparisons
trivial.
The Programmer's Control: You keep the data inside a MyTime object so you can't
accidentally add "45 seconds" to "Player Score" or "Temperature."

Comparison: Generalization vs. Specialization


Approach Logic Method Pros Cons

Specialized Check hours, then Feels like "human" High chance of "off-by-
(Nested if) minutes, then seconds. thinking. one" errors; very wordy.

Requires an extra
Generalized Convert everything to Extremely reliable;
conversion step
(Integer) total seconds. incredibly short code.
(to_seconds).
1BPLC105B Python Programming

Operator Overloading: The Final Polish


The text mentions "Operator Overloading" at the end. This is the ultimate "Computer Scientist" move.
It allows you to use standard Python operators like >, <, and + with your custom objects.
Instead of calling [Link](t2), you can make the code read like a math equation. To do this, you use
Dunder (Double Under) Methods:
gt (Greater Than): Replaces after.
eq (Equal To): Checks if two times are the same.
add (Addition): Adds two times.

Here is how that "Aha!" insight looks when fully integrated into Python's syntax:
class MyTime:

# ... previous methods ( init , to_seconds) ...


def gt (self, other):
"""Allows use of the > operator."""
return self.to_seconds() > other.to_seconds()
def eq (self, other):
"""Allows use of the == operator."""
return self.to_seconds() == other.to_seconds()
# Now the code looks like this:
if current_time > done_time:
print("The bread will be done before it starts!")

Operator Overloading
Operator overloading is where the "magic" of Python's readability really shines. By using "dunder"
(double underscore) methods, you bridge the gap between complex object logic and simple, intuitive
math.

The "Dunder" Breakdown


When you write t1 + t2, Python doesn't just see a plus sign; it looks for a specific "hook" inside your
class.
Operator Method Action in MyTime / Point

+ add Adds two objects together.

- sub Subtracts one object from another.

* mul Multiplies (Dot product if Point * Point).

*
rmul Scalar multiplication (e.g., 2 * Point).
(reversed)

The Multiplication Mystery: mul vs rmul


The AttributeError you encountered is a classic "Type Mismatch" problem. It happens because of how
Python decides which method to call based on the left-hand operand.
p2 * 2: Python calls p2. mul (2). The code inside mul expects other to be a Point so it
can do other.x. Since 2 is an integer, it crashes.
2 * p2: Python looks at 2 (an integer) and realizes it doesn't know how to multiply an integer by
a Point. It then "asks" the Point: "Do you have a way to handle being on the right side of a
multiplication?" This triggers rmul .
1BPLC105B Python Programming

The Fix: Polymorphic Multiplication


To make p2 * 2 work without crashing, you can update mul to check what other is. This makes
your class more robust:

def mul (self, other):


if isinstance(other, Point):
# Do dot product
return self.x * other.x + self.y * other.y
else:
# Assume 'other' is a scalar (like an int or float)
return Point(self.x * other, self.y * other)

Exercise Solution: sub for MyTime


Using the "Aha!" insight of converting to seconds, subtraction becomes a breeze. However, we have to
decide how to handle negative time (e.g., 1:00 AM minus 2:00 AM).

def sub (self, other):


# Convert both to seconds and subtract
diff_seconds = self.to_seconds() - other.to_seconds()
# Using our normalized init (handling negative results with modulo)
# 86400 is the number of seconds in a day
return MyTime(0, 0, diff_seconds % 86400)

Why "rmul" is named that way


The 'r' in rmul stands for reflected (or right). It’s the backup plan Python uses when the standard
left-to-right operation fails. This allows for "commutative" operations, ensuring that Point * 2 and 2 *
Point both behave reasonably.

Polymorphism
Polymorphism is one of the most powerful "superpowers" in Python. It allows you to write high-level
logic—like your multadd function—without worrying about the specific data types involved.
As long as the objects "know" how to handle the operations you're asking of them, Python is happy to
proceed.

The "Duck Typing" Philosophy


The term Duck Typing comes from the phrase:
"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a
duck."

In Python, this means we don't check if an object is a Point or a List. Instead, we check if it has a
reverse() method or an add method.

Unintentional Polymorphism in Action


Your front_and_back example is a perfect demonstration of this. You originally wrote it for lists, but
because you gave the Point class a reverse method and a str method, the function "just worked."
Why this is useful:
Code Reuse: You can write a single sorting algorithm that works for MyTime, Point, or
ProductPrice objects, as long as they all implement the < operator ( lt ).
Flexibility: You can swap out one part of your program for another without rewriting the core
logic.
1BPLC105B Python Programming

Comparison: How Different Objects "Quack"


Let's look at how the multadd(x, y, z) function behaves differently based on what we feed it:
Input Types Operation 1: x * y Operation 2: + z Result Type

Integers (3, 2, 1) 3*2=6 6+1=7 Integer

Scalar & Point (2, p1, p2) 2 * (3, 4) = (6, 8) (6, 8) + (5, 7) = (11, 15) Point

Points & Scalar (p1, p2, 1) (3, 4) . (5, 7) = 43 43 + 1 = 44 Integer

A Final Thought on copy


In your front_and_back function, you used [Link](front). This is a "shallow copy." For a simple
Point, this is fine. But if your object contained a list or another object inside it, you might need
[Link]().
Since polymorphism relies on these operations working correctly, being aware of how copy or
reverse behaves is crucial to avoiding bugs when "unintentionally" using a function.
1BPLC105B Python Programming

Chapter 3- Exceptions

Exceptions
Exception handling is the art of defensive programming. Instead of letting your program crash when
the "real world" gets messy (like a missing file or a typo from a user), you create a controlled "safety
net."

The Anatomy of the try Statement


Think of the try block as a high-wire act and the other clauses as different types of safety equipment.
try: The code you are "attempting." Python monitors this block for any "quacks" (exceptions).
except: The rescue mission. It only runs if a specific error occurs. You should always be
specific (e.g., use except ValueError) rather than using a "bare" except, which can hide bugs like
KeyboardInterrupt (preventing you from stopping the program with Ctrl+C).
else: The "Clear Skies" block. It runs only if no exceptions occurred in the try block. This is the
best place to put code that depends on the try block succeeding, but which itself shouldn't be
protected by the exception handler.
finally: The "Housekeeping" block. It runs no matter what, even if the program is about to
crash or return from a function. It is perfect for closing files or releasing database connections.

Why "Leanness" Matters


The text gives a vital piece of advice: "Keep as little as possible in the try block." Consider this
example:

try:
# Too much going on here!
val = float(input("Enter a number: "))
result = 100 / val
print("Result is", result)
except ValueError:
print("That wasn't a number!")

If this fails, was it because the input wasn't a number (ValueError) or because the user entered
0 (ZeroDivisionError)? Because we put everything in one block, the ZeroDivisionError will still crash
the program.

Improved Multi-Exception Handling


You can handle multiple specific errors to give the user better feedback:

try:
val = float(input("Enter a divisor: "))
result = 100 / val
except ValueError:
print("Error: Please enter a valid numeric value.")
except ZeroDivisionError:
print("Error: You cannot divide by zero.")
else:
print(f"Success! The result is {result}")
finally:
print("Operation complete.")
1BPLC105B Python Programming

Raising Your Own Exceptions


Sometimes, your program encounters a situation that isn't a "system" error, but is an "application" error.
For example, in our MyTime class, what if someone tries to set hours = -5? You can raise your own
exception:

def set_hours(self, h):


if h < 0 or h > 23:
raise ValueError("Hours must be between 0 and 23")
[Link] = h

This uses the same exception objects that Python uses, allowing you to use the try/except structure
for your own custom rules.

Raising your own exceptions is like setting an alarm within your code. Instead of waiting for
Python to fail on a technicality (like a math error), you are defining the business rules of your program.
If those rules are broken, you pull the "emergency brake."

Unwinding the Call Stack


The text mentions a crucial concept: Unwinding the Call Stack. When you raise an exception, Python
stops looking at the next line of code. Instead, it starts a frantic search backward through the functions
that were currently running to see if any of them have a try...except block ready to catch the error.

If it gets all the way back to the start of the program without finding a handler, it gives up and crashes
with a Traceback. This is actually a good thing! It’s much better for a program to crash than to
continue running with "bad" data (like an age of -2).

The "All-or-Nothing" Exception Trap


In the recursion_depth example you provided, notice line 5: except:.
As we discussed, this is a "bare except". While it successfully catches the RecursionError when the
stack limit is reached, it also catches everything else. If you had a typo inside the try block (like calling
printt instead of print), the bare except would hide that bug and just print "I cannot go any deeper,"
making it very hard to debug.
A safer version would be:

def recursion_depth(number):
try:
recursion_depth(number + 1)
except RecursionError: # Specifically catch the depth limit error
print("Reached the limit at depth:", number)

Custom Exceptions: Creating Your Own Error Types


Sometimes, ValueError isn't descriptive enough. If you are building a banking app, you might want an
error specifically for "Insufficient Funds." You can create your own exception by inheriting from the
base Exception class:

class AgeTooYoungError(ValueError):
"""Exception raised when an age is below the legal limit."""
pass
def verify_entry(age):
if age < 18:
raise AgeTooYoungError(f"Age {age} is under the legal limit.")
1BPLC105B Python Programming

By doing this, you can catch this specific error while letting other ValueErrors pass through to a
different handler.

Why separate line 5 and 6?


The text makes a smart point about separating the creation of the object from the raising of it.
Creation: my_error = ValueError(...) - This just creates a data object. It does nothing to the
flow of the program.
Raising: raise my_error - This is the "action" verb that triggers the unwinding of the stack.

The finally clause of the try statement


The finally clause is the "safety inspector" of your code. Its job is to ensure that no matter how chaotic
things get in the try blockwhether the code finishes perfectly or explodes with an error certain cleanup
tasks must happen.

The "Must-Run" Guarantee


In your turtle example, the finally block is crucial because computer resources (like windows, file
handles, or network sockets) are limited. If your program crashes without closing the window, that
window might stay "zombie-like" on your screen, or the memory it uses might not be released back to
the system.

Handled vs. Unhandled Exceptions


It's important to catch the distinction the text makes at the end: finally does not "catch" the error.
If you have no except block: The error occurs, the finally block runs its cleanup, and then the
error continues "unwinding the call stack" until it crashes the program.
If you have an except block: The error is caught, the except code runs, then the finally code
runs, and the program continues running normally.

A More Robust show_poly


To make your code even better, you could combine the except and finally clauses. This allows you to
handle the user's mistake (like entering "abc" instead of "5") and ensure the window closes.

def show_poly():
win = None
try:
win = [Link]()
tess = [Link]()
n = int(input("How many sides? "))
angle = 360 / n
for i in range(n):
[Link](50)
[Link](angle)
except ValueError:
print("Oops! That wasn't a valid number.")
except ZeroDivisionError:
print("A polygon must have at least 1 side!")
finally:
if win:
[Link]()
print("Window closed safely.")
1BPLC105B Python Programming

The "With" Statement (The Modern Alternative)


For many resources like files, Python developers often use the with statement (called a Context
Manager). It effectively creates a try...finally block for you automatically.

# Instead of try...finally to close a file:


with open("[Link]", "r") as f:
content = [Link]()
# The file is automatically closed here, even if an error occurred!

You might also like