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

Python Notes Part 2

This document covers advanced Python concepts including functions, file I/O, error handling, and object-oriented programming (OOP). It emphasizes the importance of functions for code reusability, explains file operations and error management, and introduces OOP principles such as classes, inheritance, and methods. Additionally, it highlights common pitfalls for beginners, particularly around mutable vs immutable types.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views15 pages

Python Notes Part 2

This document covers advanced Python concepts including functions, file I/O, error handling, and object-oriented programming (OOP). It emphasizes the importance of functions for code reusability, explains file operations and error management, and introduces OOP principles such as classes, inheritance, and methods. Additionally, it highlights common pitfalls for beginners, particularly around mutable vs immutable types.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Python Notes ,Part 2

Functions • File I/O & Error Handling • OOP • Tricky Areas


the stuff that separates beginners from real programmers
1. Functions
🎓 If you're writing the same block of code more than once, you've already made a mistake.
That's what functions are for, write it once, use it everywhere. This is the single most
important concept in all of programming.

What Is a Function, Really?


A function is a named, reusable block of code. You define it once. You call it as many times as
you want, from anywhere in your program, with different inputs each time. That's it. That's the
whole idea.
Without functions, every program would be a giant waterfall of code ,impossible to read,
impossible to fix, impossible to build on. Functions are how we break big problems into
manageable pieces.

Defining a Function-the def keyword


The syntax is dead simple. def, a name, parentheses, a colon, then an indented block of code:

def greet():
print("Hello, student!")

# nothing happens until you CALL the function


greet() # now it runs
greet() # runs again ,same function, called twice

📌 Note: Defining a function does not run it. Calling it does. This trips up almost every
beginner once.

Parameters vs Arguments ,know the difference


Parameters are the variable names in the function definition ,they're placeholders. Arguments
are the actual values you pass when calling the function. Most people use these words
interchangeably and that's fine in conversation, but when you're reading error messages,
knowing the distinction matters.

# 'name' and 'score' are PARAMETERS ,just placeholders


def print_result(name, score):
print(f"{name} scored {score}/100")

# 'Alice' and 85 are ARGUMENTS ,the real values


print_result('Alice', 85)
print_result('Bob', 92) # same function, different arguments
return vs print ,one of the most misunderstood things in Python
🎓: I have seen students fail interviews because they didn't understand return vs print. This is
not a small thing. Read this section twice.

print() shows something on the screen. That's ALL it does. The value is gone after that ,you
can't use it anywhere else.
return sends a value BACK to wherever the function was called from. You can store it, use it in
calculations, pass it to another function. It's how functions communicate with the rest of your
program.

# using print inside a function ,the result is LOST


def add_wrong(a, b):
print(a + b) # displays it, but the value disappears

result = add_wrong(3, 4) # prints 7


print(result) # None ,there's nothing to store!

# using return ,the result is KEPT


def add_right(a, b):
return a + b # sends the value back to the caller

result = add_right(3, 4) # result is now 7


print(result) # 7
print(result * 2) # 14 ,we can use it!

⚠️Watch out: A function that doesn't have a return statement always returns None. Always.
If you call a function and store the result but get None, a missing return is the first thing to
check.

Default Parameters
Sometimes you want a parameter to have a fallback value if the caller doesn't provide one.
Default parameters handle this ,define them with = in the function signature:

def greet_student(name, greeting='Hello'):


print(f"{greeting}, {name}!")

greet_student('Alice') # Hello, Alice!


greet_student('Bob', 'Good morning') # Good morning, Bob!
greet_student('Charlie', 'Yo') # Yo, Charlie!

📌 Note: Required parameters (no default) must come BEFORE default parameters. def
f(x=1, y) is a SyntaxError.

*args ,when you don't know how many arguments there will be
The * before a parameter name tells Python: collect all the extra positional arguments into a
tuple and call it args. The name 'args' is just convention ,what matters is the *.

def total(*args):
print(f'received: {args}') # args is a tuple
return sum(args)

print(total(10, 20)) # received: (10, 20) → 30


print(total(1, 2, 3, 4, 5)) # received: (1, 2, 3, 4, 5) → 15
print(total(100)) # received: (100,) → 100

**kwargs ,keyword arguments


The ** collects all extra keyword arguments into a dictionary. kwargs stands for keyword
arguments ,again, the name is just convention:

def build_profile(**kwargs):
for key, value in [Link]():
print(f' {key}: {value}')

build_profile(name='Mellow', age=20, major='CS')


# name: Mellow
# age: 20
# major: CS

# combining all three


def show_all(required, *args, **kwargs):
print(f'required: {required}')
print(f'extras: {args}')
print(f'keywords: {kwargs}')

show_all('hello', 1, 2, 3, x=10, y=20)

💡 Pro tip: The order must always be: normal params → *args → **kwargs. Python enforces
this strictly.

2. File I/O and Error Handling


🎓: Every real-world program reads or writes files. Every real-world program also
crashes ,unless you handle errors properly. These two topics are where you stop writing toy
programs and start writing real ones.

Reading and Writing Files ,open()


The open() function opens a file and returns a file object you can read from or write to. It takes a
filename and a mode:
Mode Meaning What happens if file doesn't exist
"r" Read (default) Raises FileNotFoundError
"w" Write (overwrites) Creates the file
"a" Append (adds to end) Creates the file
"r+" Read + Write Raises FileNotFoundError

Writing to a File

# always use 'with' ,it automatically closes the file when done
with open('[Link]', 'w') as file:
[Link]('Alice: 85\n')
[Link]('Bob: 92\n')
[Link]('Charlie: 78\n')

# the file is now closed automatically ,no need to call [Link]()

📌 Note: The \n is a newline character ,it moves to the next line. Without it, everything gets
written on one line.

Reading from a File

# read the entire file as one big string


with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# read line by line ,better for large files


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # .strip() removes trailing \n

# read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]() # ['Alice: 85\n', 'Bob: 92\n', ...]

Appending to a File

# 'a' mode adds to the end ,doesn't erase existing content


with open('[Link]', 'a') as file:
[Link]('Diana: 95\n')

# 'w' mode would have ERASED the file first


# never use 'w' when you want to keep existing data
try / except ,Catching Errors
🎓: Here's the truth about errors: they will happen. A file won't exist. A user will type a letter
when you expected a number. A network request will time out. The question isn't whether
errors happen ,it's whether YOUR CODE handles them gracefully or crashes spectacularly.

try/except is how Python handles errors (called exceptions) without crashing:

try:
# code that MIGHT fail goes here
number = int(input('enter a number: '))
result = 100 / number
print(f'100 / {number} = {result}')

except ValueError:
# runs if the user typed something that isn't a number
print('that is not a valid number.')

except ZeroDivisionError:
# runs if the user typed 0
print('you cannot divide by zero.')

Common Exception Types

Exception When it happens


ValueError Wrong type of value ,e.g. int('hello')
TypeError Wrong type ,e.g. 'text' + 5
FileNotFoundError File doesn't exist when opening with 'r'
ZeroDivisionError Dividing by zero
IndexError List index out of range ,e.g. list[99]
KeyError Dictionary key doesn't exist ,e.g. d['missing']
AttributeError Method doesn't exist on that type

try / except / else / finally ,the full picture

try:
file = open('[Link]', 'r')
content = [Link]()

except FileNotFoundError:
# runs ONLY if an error occurred
print('file not found!')
content = ''

else:
# runs ONLY if NO error occurred
print(f'file loaded: {len(content)} characters')

finally:
# runs NO MATTER WHAT ,error or not
# perfect for cleanup: closing files, database connections, etc.
print('done attempting to open file.')

💡 Pro tip: The 'with' statement handles finally automatically for files ,it always closes the file
even if an error occurs. Always use 'with open(...)' over manual open/close.

Raising Your Own Exceptions


You can trigger exceptions yourself using raise ,useful for enforcing rules in your own functions:

def set_score(score):
if score < 0 or score > 100:
raise ValueError(f'score must be 0-100, got {score}')
return score

try:
set_score(150)
except ValueError as e:
print(f'invalid input: {e}')
# invalid input: score must be 0-100, got 150

3. Object-Oriented Programming (OOP)


🎓: OOP is not a Python thing. It's a way of thinking about software. Once it clicks, you'll
never look at code the same way again. Everything becomes objects ,things with properties
and behaviours. Your entire operating system is built on this idea.

The Core Idea ,What Is a Class?


Think about a student. Every student has a name, an age, a student ID, a GPA. Every student
can enrol in courses, submit assignments, graduate. These are the student's properties (data)
and behaviours (actions).
A class is a blueprint that defines what a student looks like and what a student can do. An object
is one specific student built from that blueprint. You can have a thousand students ,a thousand
objects ,all built from the same class.

# class = the blueprint


class Student:
pass # empty for now

# object = a specific instance built from the blueprint


alice = Student()
bob = Student()
# alice and bob are two separate objects, both of type Student

__init__ ,The Constructor


__init__ is a special method that runs automatically the moment you create a new object. It's
where you set up the object's initial data. The double underscores (called 'dunder') mean Python
calls this method automatically ,you never call __init__ directly.

class Student:
def __init__(self, name, age, student_id):
# [Link] means 'this object's name'
[Link] = name
[Link] = age
self.student_id = student_id
[Link] = 0.0 # default value ,not passed in
[Link] = [] # start with empty list

# creating objects ,__init__ runs automatically


alice = Student('Alice', 20, 'S001')
bob = Student('Bob', 22, 'S002')

print([Link]) # Alice
print(bob.student_id) # S002
print([Link]) # []

self ,what is it?


self is a reference to the specific object being worked with. When you call [Link](), Python
automatically passes alice as the first argument ,that's self. It's how the method knows which
object's data to use.
📌 Note: You must always include self as the first parameter of every method. But you never
pass it yourself when calling ,Python does that automatically.

class Student:
def __init__(self, name, gpa):
[Link] = name
[Link] = gpa

def introduce(self): # self = the specific student calling this


print(f'Hi, I am {[Link]} with a GPA of {[Link]}')

def is_honour_roll(self):
return [Link] >= 3.5

alice = Student('Alice', 3.8)


[Link]() # Hi, I am Alice with a GPA of 3.8
print(alice.is_honour_roll()) # True

Attributes vs Methods

Term What it is Example


Attribute Data stored on an object ,a [Link], [Link]
variable
Method A function defined inside a class [Link](), [Link]()
,an action
Class attribute Shared by ALL objects of that [Link] = 'BIUST'
class
Instance attribute Belongs to ONE specific object [Link] set in __init__

class Student:
university = 'BIUST' # class attribute ,same for ALL students

def __init__(self, name):


[Link] = name # instance attribute ,different per student

def enrol(self, course): # method ,an action


print(f'{[Link]} enrolled in {course}')

alice = Student('Alice')
bob = Student('Bob')

print([Link]) # BIUST ,from the class


print([Link]) # BIUST ,same shared value
print([Link]) # Alice ,her specific name
[Link]('CS101') # Alice enrolled in CS101

Inheritance ,Reusing and Extending Classes


🎓: Inheritance is one of the most powerful ideas in OOP. Instead of rewriting everything
from scratch, you take an existing class and say 'build on top of this'. The child class gets
everything the parent has ,for free.

Here's the syntax: class Child(Parent). The child inherits all of the parent's attributes and
methods, and can add its own or override the parent's:

# Parent class
class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

def introduce(self):
print(f'I am {[Link]}, age {[Link]}')

# Child class ,inherits from Person


class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age) # calls Person's __init__
self.student_id = student_id
[Link] = []

# new method ,only Students have this


def enrol(self, course):
[Link](course)
print(f'{[Link]} enrolled in {course}')

# override ,Student's version replaces Person's


def introduce(self):
print(f'I am {[Link]}, student #{self.student_id}')

class Lecturer(Person):
def __init__(self, name, age, department):
super().__init__(name, age)
[Link] = department

def teach(self, course):


print(f'Dr. {[Link]} is teaching {course}')

alice = Student('Alice', 20, 'S001')


dr_k = Lecturer('Kgosi', 45, 'Computer Science')

[Link]() # I am Alice, student #S001 (overridden)


[Link]('CS101')
dr_k.introduce() # I am Kgosi, age 45 (from Person)
dr_k.teach('CS101')

📌 Note: super() refers to the parent class. super().__init__() calls the parent's
constructor ,you do this so the parent's setup code runs too.
💡 Pro tip: A good rule: if two things share properties and behaviours, one probably inherits
from the other. Student and Lecturer both share name and age ,they both inherit from
Person.

4. Commonly Tricky Areas for Beginners


🎓: These are the topics that will silently break your code in ways that make no sense ,until
you understand what's actually happening underneath. Master these and you'll debug 80%
faster than the average beginner.

Mutable vs Immutable Types


This is probably the most important concept you didn't know you needed to know. Every value
in Python is either mutable (can be changed in place) or immutable (cannot be changed ,a new
object is always created instead).

Immutable (cannot be Mutable (can be changed in place)


changed)
int, float, bool list
str dict
tuple set

Why does this matter? Because when you assign a mutable object to a new variable, you're not
making a copy ,you're creating a second name pointing to the SAME object in memory:

# IMMUTABLE ,assignment creates a NEW object


a = 10
b = a
b = 20 # b now points to a new int object
print(a) # 10 ,a is completely unaffected

# MUTABLE ,assignment creates a second name for the SAME object


list1 = [1, 2, 3]
list2 = list1 # list2 is NOT a copy ,it IS list1
[Link](4) # changing list2 also changes list1!
print(list1) # [1, 2, 3, 4] ,surprise!

# to make a real copy, use .copy()


list3 = [Link]()
[Link](99)
print(list1) # [1, 2, 3, 4] ,list1 is safe now

⚠️Watch out: This catches everyone. If you pass a list to a function and the function
modifies it, the original list is modified too ,because they're the same object. Use .copy()
when you need independence.

List Comprehensions
A list comprehension is a compact, readable way to create a new list by transforming or filtering
an existing one. Once you get used to them, writing a for loop just to build a list feels clunky.
The formula: [ expression for item in iterable if condition ]
# the traditional way ,4 lines
squares = []
for x in range(1, 6):
[Link](x ** 2)
# squares = [1, 4, 9, 16, 25]

# comprehension ,same result, 1 line


squares = [x ** 2 for x in range(1, 6)]

# with a filter ,only even numbers


evens = [x for x in range(20) if x % 2 == 0]

# transform strings
names = ['alice', 'bob', 'charlie']
upper = [[Link]() for name in names]
# ['ALICE', 'BOB', 'CHARLIE']

# filter a list of dicts


students = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 45},
{'name': 'Sara', 'score': 91},
]
passed = [s['name'] for s in students if s['score'] >= 60]
# ['Alice', 'Sara']

💡 Pro tip: If your comprehension needs more than one if condition or gets longer than one
comfortable line, just write a regular loop. Readability always wins.

Scope ,Local vs Global


Scope is about WHERE a variable exists and WHERE it can be accessed. Variables created
inside a function only exist inside that function ,they die when the function ends. Variables
created outside all functions are global ,visible everywhere.

name = 'Global Mellow' # global variable ,exists everywhere

def show_name():
name = 'Local Mellow' # local variable ,only inside this function
print(name) # Local Mellow (local shadows global)

show_name() # Local Mellow


print(name) # Global Mellow ,global is untouched

# trying to MODIFY a global variable from inside a function


count = 0

def increment():
count += 1 # ❌ UnboundLocalError!
# Python treats count as local when you try to assign
to it
def increment_fixed():
global count # tell Python: I mean the global count
count += 1 # ✅ now it works

increment_fixed()
print(count) # 1

⚠️Watch out: Using global is generally considered bad practice. It makes code hard to
track ,you never know what changed count. The better approach is to pass count as an
argument and return the new value.

# the GOOD way ,pass in, return out


def increment(count):
return count + 1

count = 0
count = increment(count) # count is now 1
count = increment(count) # count is now 2

== vs is ,equality vs identity
These look like they should do the same thing. They do not.

Operator Question it answers Compares


== Do these two have the same The contents
VALUE?
is Are these two the SAME The memory address
OBJECT in memory?

# == checks value ,are the contents equal?


a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True ,same contents
print(a is b) # False ,two separate list objects in memory

# is checks identity ,is it literally the same object?


c = a # c points to the SAME object as a
print(a is c) # True ,same object, same memory address

# the only safe use of 'is' for values: checking for None
value = None
if value is None: # ✅ correct way to check for None
print('nothing here')

if value == None: # works but not recommended


print('nothing here')
⚠️Watch out: Never use 'is' to compare strings, numbers, or lists. Python caches small
integers and short strings, which makes 'is' appear to work sometimes ,but it's unreliable
and will eventually bite you in unexpected ways.

# this looks right but is unreliable


x = 256
y = 256
print(x is y) # True ,Python caches small integers (happens to work)

x = 1000
y = 1000
print(x is y) # False ,large integers aren't cached

# lesson: use == for values, is for None/True/False only


Quick Reference Part 2

Topic Key Syntax The One Thing to Remember


def def name(params): Definition does nothing ,calling it does.
return return value Return sends a value back. print() just displays
it.
Default params def f(x=10): Defaults go at the end of the parameter list.
*args def f(*args): Collects extra positional args into a tuple.
**kwargs def f(**kwargs): Collects extra keyword args into a dict.
File write open('f', 'w') Always use 'with'. 'w' overwrites, 'a' appends.
File read open('f', 'r') FileNotFoundError if file doesn't exist.
try/except try: ... except E: Catch specific exceptions ,not bare except:.
finally finally: Runs no matter what. Use for cleanup.
class class Name: Blueprint. Defines what objects look like.
__init__ def __init__(self): Runs on object creation. Sets up attributes.
self [Link] Always the first param. Points to THIS object.
Inheritance class Child(Parent): Child gets everything Parent has.
super() super().__init__() Call parent's constructor from child.
Mutable list, dict, set Assignment copies the REFERENCE, not the
data.
Immutable int, str, tuple Changes always create a NEW object.
Comprehension [x for x in list] Compact list creation ,add 'if' to filter.
Scope global keyword Prefer passing args and returning values over
global.
== vs is == for values Use 'is' only for None, True, False checks.

You might also like