Learn Python in 86400 Seconds


Welcome to 86400Sec! This course is designed to take you from zero to hero in Python programming in just 8 days. Each day features 3 hours of focused learning, breaking Python concepts into digestible lessons and practical examples. By the end of this course, you'll have the skills to build your own Python programs and tackle real-world problems.

Course Overview

  • Total Duration: 86400 seconds (24 hours)
  • Structure: 3 hours/day over 8 days
  • Audience: Beginners who want to learn Python from scratch.

Learning Outcomes

  • Python fundamentals: syntax, data types, and operations.
  • Advanced concepts like functions, classes, and error handling.
  • Data structures: tuples, lists, sets, and dictionaries.

Curriculum

  • Day 1:

    • Introduction to Python
    • Variables and data types
    • Working with Strings
  • Day 2:

    • Understanding tuples
    • Working with lists
    • Introduction to sets
    • Basic operations: slicing, indexing, iteration
  • Day 3:

    • Understanding and Working with Dictionaries
  • Day 4:

    • Understanding Control flow statements
    • Working with Conditional Statements and Loops
  • Day 5:

    • Writing your own functions
    • Parameters, arguments, and return values
  • Day 6:

    • Understanding Error Handling Concept
    • Understanding exceptions
    • Using try, except, and finally
    • Raising and customizing exceptions
  • Day 7:

    • Classes and Object-Oriented Programming (OOP)
    • Introduction to OOP
    • Creating classes and objects
    • Attributes and methods
  • Day 8:

    • File Handling

Requirements

  • To follow along with this course, you'll need:
    • Python
    • Jupyter Notebook
pip install notebook  

Getting Started

1- Clone this repository to your local machine:

git@github.com:Zemzemfiras1/PythonIN-86400sec.git

2- Navigate to the course directory:

cd PythonIN-86400sec

3- Launch Jupyter Notebook:

jupyter notebook

4-Open the notebooks for each day's lessons and follow along.


Contributing

  • Contributions, bug reports, and feature requests are welcome!
  • Feel free to fork this repository and submit a pull request.

Python Basics

What is Python?

Python is a versatile, high-level programming language that is easy to learn and use. It is widely used in various domains such as web development, data science, machine learning, and more....

Why Use Python?

  • Simple and readable syntax.
  • Huge standard library.
  • Cross-platform compatibility.

Section 1: Variables and Data Types

Variables

In Python, variables are used to store data values, and the type of each variable is automatically determined based on the value assigned to it

  • Example: Variables & its Types
# int: Integer type, stores whole numbers.
int: 10

# float: Floating-point type, stores numbers with decimal points.
float: 3.14

# complex: Complex number type, stores numbers with both real and imaginary parts.
complex: (1+2j)

# str: String type, stores sequences of characters (text).
str: Hello, World!

# list: List type, an ordered and mutable collection of elements.
list: [1, 2, 3, 'apple']

# tuple: Tuple type, an ordered but immutable collection of elements.
tuple: (1, 2, 3, 'banana')

# dict: Dictionary type, stores key-value pairs.
dict: {'key1': 'value1', 'key2': 42}

# set: Set type, stores unordered collections of unique elements.
set: {1, 2, 3, 4}

# frozenset: Frozenset type, an immutable version of a set.
frozenset: frozenset({1, 2, 3, 4})

# bool: Boolean type, stores either True or False.
bool: True

# bytes: Bytes type, stores immutable sequences of bytes.
bytes: b'Hello'

# NoneType: Represents the absence of a value or a null value.
NoneType: None

Numeric and floats

- Create 2 variable a & b , then assign them numeric values eqaul to 77 and 3 
a = 77
b = 3
- print them , Then make 4 new variables :
    - sum_ab           # sum        => + 
    - diff_ab          # Difference => - 
    - prod_ab          # Product    => * 
    - quot_ab          # Qutient    => /
# print the value assined to the variable "a"
print(a) 
print ("The type of the value ",a," is :", type(a))
# print the value assined to the variable "b"
print(b) 
print ("The type of the value ",b," is :", type(b))

sum_ab = a + b
print(sum_ab)
diff_ab = a - b
print(diff_ab)
prod_ab = a * b
print(prod_ab)
quot_ab = a / b
print(quot_ab)
#check the type of the value quot_ab : 
print(type(quot_ab))

Section 2 :String Manipulation

Strings are sequences of characters enclosed in either single (') or double (") quotes in Python. String manipulation refers to the various operations you can perform on strings, such as concatenation, slicing, modification, and more. Python provides a variety of built-in methods that allow you to work with strings efficiently.

str1 = "Hello"
str2 = "Learner"

print(type(str1))
print(type(str2))

Concatination

without_space = str1+str2
print(without_space)

Add a space between the 2 strings

with_space = str1+" "+str2
print(with_space)

Length

Getting the number of characters in a string using len()

print("Length of Hello = ",len(str1))
print("Length of Learner = ",len(str2))
print("Length of 'Hello Learner' = ",len(with_space)) 

Upper & Lowercase


print(str1.lower())  # Expected output: hello
print(str2.upper())  # Expected output: LEARNER

Check for substring

text = "consistency if a picotal key in Learning "
# Check if lazy exist in text ?? 
print("Lazy" in text)  
# Check if Cons exist in text ?? 
print("Cons" in text)  
# Check if consistency exist in text ?? 
print("consistency" in text)  

Replace Substring

text = "I love coding with R ."
new_text = text.replace("R", "Python")
print(new_text) 

Remove Whitspaces

sentence = "   Python is awesome !"
print(sentence)
clean_sentence = sentence.strip()
print(clean_sentence)

Split String

Given the string "apple,banana,cherry", split it into a list of fruits.

fruits = "apple,banana,cherry"
print(fruits) 
splitted_fruits = fruits.split(",")
print(splitted_fruits)  # Expected output: ['apple', 'banana', 'cherry']

Join Strings

words = ['I', 'Love', 'snakes']
sentence = " ".join(words)
print(sentence)  

Count Occurrences

seq = "ATCGGGCATACG"
count = seq.count("G")
print(count)  

String Slicing

Slicing allows you to extract a portion of a string by specifying the start and end indices. You can also specify a step, which allows you to skip characters.

string[start:end:step]
  • Basic Slicing
text = "Python Programming"

# Slicing from index 0 to 5 (exclusive)
result = text[0:5]
print(result)   # This slices the string from index 0 to 4 (because the end index is exclusive).


This slices the string from index 0 to 4 (because the end index is exclusive).

  • Slicing with Step
text = "Python Programming"

# Slicing from index 0 to 12 with a step of 2
result = text[0:12:2]
print(result)  # Output: Pto rg

This extracts every second character starting from index 0 up to index 11. The characters selected are "P", "t", "o", " ", "r", "g".

Omitting Start and End

You can omit the start and end values to slice from the beginning or to the end of the string:

text = "Python Programming"

# Slicing from the start to index 6 (exclusive)
result1 = text[:6]
print(result1)  # Output: Python
# Slicing from index 7 to the end of the string
result2 = text[7:]
print(result2)  # Output: Programming

Explanation:

[:6] slices from the beginning of the string up to (but not including) index 6.

[7:] slices from index 7 to the end of the string.

Negative Indices

In Python, negative indices can be used to slice the string from the end.

text = "Python Programming"

# Slicing from the second last character to the end
result = text[-2:]
print(result)  # Output: ng

Explanation: Negative indices count from the end of the string. Here -2 starts from the second last character and slices to the end of the string.

Reverse Slicing

You can also use negative steps to slice a string in reverse order.

text = "Python Programming"

# Slicing in reverse order
result = text[::-1]
print(result)  # Output: gnimmargorP nohtyP

Explanation: [::-1] reverses the string by stepping backwards through every character.


ENJOY !!

Section 3 : Lists

A list is a mutable, ordered collection of items. A list can contain elements of different data types, including numbers, strings, and even other lists.

my_list = [item1, item2, item3, ...]
  • Items in the list are separated by commas.
  • Lists are ordered, meaning the items have a defined order, and this order will not change unless you specifically reorder them.
  • Lists are mutable, meaning you can change their contents (add, remove, or modify elements)

Basic Operations on Lists:

my_list = [1, 2, 3, 5,0 ,77]
print(my_list)
Length of the list:

Use len() to get the number of elements in a list

print(len(my_list))  # Prints the length of the list
Accessing elements: You can access elements in a list using their index.
print(my_list[0])  # Prints item 1
print(my_list[1])  # Prints item 2
print(my_list[5])  # Prints the last item 
List Slicing

list slicing allows you to access a portion (sublist) of a list by specifying a range of indices. The syntax for slicing is:

        my_list[start:end]
  • start: The index of the first element you want to include in the slice.
  • end: The index of the element where the slice should stop before. (The slice will not include this index.)
  • To Better understand it lets print the indexes along with the values is to use the built-in enumerate() function, which returns both the index and the value as a tuple during iteration.
print("my list is : ", my_list)

for index, value in enumerate(my_list):
    print(f"Index {index}: {value}")
print(my_list[1:4])

Another trick is to start counting comma from where to start and where it should end

my list is :  [1 , 2 , 3 , 5 , 0 , 77]

comma indexes [v 1 v 2 v 3 v 4 v 5 v 6]

Lets Try to print the item3 > item 6

print(my_list[2:6])

Start Printing from the first item

print(my_list[0:])

Start Printing from the 3rd item

print(my_list[2:])

Print until the 3rd item

print(my_list[:3])

Print The last item

print(my_list[-1])

You can also print the entire ist using :

my_list[:] 
Add an element (Item)

You can add elements to the list using append() funciton which allow the user to add an element to the end of the list.

my_list.append(100)  # Adds 100 at the end of the list
print(my_list)

You can add elements to the list using insert() funciton which allow the user to add an element at a specific position (index) in the list.

Syntax :

list.insert(index, element)
my_list.insert(1,88)  # Adds 4 at the end of the list
print(my_list)

print(my_list)

Remove an element (Item)

You can remove elements using remove() which Removes the first occurrence of the specified value from the list

my_list.remove(100)
print(my_list)

You can remove elements using del() which deletes an element at a specified index or the entire list.

del list[index]
del list[start:end]   # For slicing
del list              # To delete the entire list
del my_list[0]
print(my_list)

You can remove elements using pop() which removes and returns the element at the specified index (or the last element if no index is provided)

list.pop(index)
my_list.pop()
print(my_list)


Section 4 : Tuples

A tuple is a collection of ordered, immutable elements in Python. Tuples are defined by enclosing elements in parentheses () and are often used for fixed collections of related data.

  • Structure :
my_tuple = (Value1, Value2, Value3, ....)

Tuples in Python are immutable, which means you cannot modify a tuple after it is created. This includes adding, removing, or changing individual elements. Immutability makes tuples:

  • Safer to use as constants, preventing accidental changes.
  • Hashable, allowing them to be used as keys in dictionaries or elements in sets.

Creating a tuple

  • example 1 :
tuple1 = (42, "hello", 3.14)
print(tuple1)
  • example 2 : use tuple() instructor
tuple2 = tuple(["42", "hello", "3.14"])
print(tuple2)

Accessing Tuple Elements

Tuples support indexing to access individual elements

my_tuple = (10, 20, 30, 40)
print(my_tuple[0])  # First element
print(my_tuple[-1]) # Last element

Slicing Tuples

Tuples can also be sliced to create subsets

my_tuple = (0, 1, 2, 3, 4, 5)
print(my_tuple[1:4])  # Elements from index 1 to 3
print(my_tuple[:3])   # First three elements

Concatenation

t1 = (1, 2)
t2 = (3, 4)
t3 = t1 + t2
print(t3) 

Repetition

t = (1, 2)
print(t * 3)  

Unpacking

Assign tuple elements to variables:

t = (1, 2, 3)
a, b, c = t
print(a, b, c)  

occurrences

Using count(): Returns the number of occurrences of a value.

t = (1, 2, 2, 3)
print(t.count(2))  

Indexing

Using index(): Returns the index of the first occurrence of a value.

print(t.index(3))   

Converting Between Tuples and Other Types

Convert a list to a tuple:

lst = [1, 2, 3]
t = tuple(lst)
print(t) 

Convert a tuple to a list:

t = (1, 2, 3)
lst = list(t)
print(lst)  

Nested Tuples

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

Comparison

tuples are compared lexicographically, meaning they are compared element by element from left to right until a difference is found.

Key Notes on Tuple Comparisons:

  • If the first elements are equal, the comparison moves to the next elements.
  • If all elements are equal up to the shorter tuple's length, the shorter tuple is considered smaller.
  • Lexicographic comparison mimics how words are compared in a dictionary.
t1 = (1, 2, 3)
t2 = (0, 1, 4)
print(t1 < t2)  # Output: False

Tuple Comparison Process Given:

t1 = (1, 2, 3)

t2 = (0, 1, 4)

Compare the first elements:

t1[0] is 1, and t2[0] is 0.

Since 1 > 0, the comparison stops here.

The result of the comparison is determined solely by this first element comparison: 1 > 0.

Outcome : Since t1[0] > t2[0], the condition t1 < t2 is False.

(1, 2, 3) < (1, 2, 4)  # True because 3 < 4

(1, 2) < (1, 2, 0)     # True because shorter tuples are compared only to their length

(0, 4, 5) < (1, 2, 3)  # True because 1 > 0

Deleting Tuples

t = (1, 2, 3)
del t
print(t)

Section 5 : Sets

What are Sets?

  • A set is a collection of unique and unordered elements in Python.

  • Defined using {} or the set() function.

  • Create an empty set named myset :

my_set = set()

# print my_set 
print(my_set)

# check my_set type 
print(type(my_set))

Adding Elements to Sets

  • Lets create a simple set which contains these n° 1,2,3,4,5 :
my_set = {1, 2, 3, 4, 5}
print(my_set)

  • Add add an element to my set eg. 5
new_set = my_set.add(0)  
print(new_set)  

Explanation : The reason new_set = my_set.add(5) gives you None is that the .add() method in Python does not return any value; it modifies the set in place. When a method doesn't return anything explicitly, Python implicitly returns None.

  • Lets try
my_set.add(0)  
print(my_set)  
  • Adding multiple elements to a set using the update()
print("Initial set :",my_set)

# Add multiple elements
my_set.update([6, 7, 8])

print("Updated set :", my_set)

Removing Elements from Sets

  • To remove an element for a set , you may use either .remove() which Raises a KeyError if the element is not found.
  • Use Case: When you're certain the element exists in the set.
print("Initial set : ", my_set)
# Remove n° 3
my_set.remove(3)
print(my_set)
  • Re-run the above cell and checkout what is the output

  • To remove an element for a set , you may use either .discard() Does not raise an error if the element is not found; it simply does nothing.

  • Use Case: When you're not sure if the element exists and want to avoid errors.

  • Try to remove the non-existant element "3" removed before

# Remove n° 3
my_set.discard(3)
print(my_set)

Summary | Method | Behavior if the element is not in the set | Raises Error? | Use Case | |-------------|-------------------------------------------|---------------|----------------------------------------| | .remove() | Raises a KeyError. | Yes | Use when you're sure the element exists. | | .discard() | Does nothing. | No | Use when you're not sure if the element exists. |

Membership Testing

Check if an element exists in a set using the in keyword.

print(my_set)
print(3 in my_set)  
print(6 in my_set)  

### Mathematical Operations Mathematical Operations on Sets :

  1. Union (| or .union())
  2. Intersection (& or .intersection())
  3. Difference (- or .difference())
  4. Symmetric Difference (^ or .symmetric_difference())
set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}

Union

print("set_a",set_a)
print("set_b",set_b)

print("Union:", set_a | set_b)

Intersection

print("set_a",set_a)
print("set_b",set_b)

print("Intersection:", set_a & set_b)

Difference

print("set_a",set_a)
print("set_b",set_b)

# This returns elements that are in set_a but not in set_b
# unique to set_a 

print("Difference (A - B):", set_a - set_b) 

#This returns elements that are in set_b but not in set_a.
# unique to set_b

print("Difference (B - A):", set_b - set_a)

Symmetric Difference

#The symmetric difference consists of the parts of A and B that do not overlap.
print("Symmetric Difference:", set_a ^ set_b)

Copying Sets

Copying sets is essential when you want to work with a new set derived from an existing set without altering the original set. Without copying, any modifications to the new set would also affect the original set (if both sets reference the same memory location). By creating a copy, you ensure that the original data remains unchanged.

original_set = {1, 2, 3}
copy_set = original_set  # No copying, both refer to the same set
copy_set.add(4)

print("Original Set:", original_set)  # Output: {1, 2, 3, 4}
print("copy Set:", copy_set)           # Output: {1, 2, 3, 4}

  • remove the element 4 from the copied set
print("Original Set:", original_set)
print("copy Set:", copy_set)     
copy_set.remove(4)
print("modified_set :",copy_set)

Sets as Filters

  • Create an empty set named set_with_duplicates containing {1, 2, 2, 3, 4, 4, 5}:
set_with_duplicates = {1, 2, 2, 3, 4, 4, 5}
print("Set removes duplicates and only let unique element:", set_with_duplicates)

Summary

Key Differences Between List, Tuple, and Set

FeatureListTupleSet
DefinitionOrdered, mutable collection of elements.Ordered, immutable collection of elements.Unordered, mutable collection of unique elements.
SyntaxDefined using square brackets [].Defined using parentheses ().Defined using curly braces {} or the set() function.
OrderMaintains the order of elements.Maintains the order of elements.Does not maintain any specific order.
MutabilityMutable: Can add, remove, or modify elements.Immutable: Elements cannot be changed after creation.Mutable: Can add or remove elements, but not change individual values.
DuplicatesAllows duplicate elements.Allows duplicate elements.Does not allow duplicate elements.
PerformanceSlower than tuples due to mutability.Faster than lists for iterations due to immutability.Faster lookups and membership tests due to hashing.
Use CaseSuitable for collections that may change.Suitable for fixed collections of data.Suitable for collections of unique items.
MethodsExtensive: e.g., append(), remove().Limited: e.g., count(), index().Specialized: e.g., add(), remove(), union(), intersection().
HomogeneityCan store heterogeneous elements.Can also store heterogeneous elements.Can also store heterogeneous elements, but all elements must be hashable.
Example[1, 2, 3](1, 2, 3){1, 2, 3} or set([1, 2, 3])

ENJOY !!

Section 6 : Dictionaries

A Python dictionary is an unordered collection of items. It is a collection of key-value pairs, where each key is unique, and the value can be any Python object.

my_dict = {key1: value1, key2: value2, key3: value3 ... }

    or

my_dict = dict(key1=value1, key2=value2, key3=value3 ...)

Creating a Dictionary

To create a dictionary, you can use curly braces {} or the dict() constructor.

my_dict_exp1 = {'name': 'Alice', 'age': 25, 'city': 'New York'}
print(my_dict_exp1)

my_dict_exp2 = dict(name='Firas', work='PhD Student', city='Tunisia')
print(my_dict_exp2)

Accessing Values by Key

using square brackets []

You can access the value associated with a specific key by using square brackets [].

print(my_dict_exp1['name'])  

Using the get() Method

The get() method returns the value for the given key if it exists, otherwise, it returns None (or a default value if provided).

# get the value assigned to the key age in my_dict_exp1

print(my_dict_exp1.get('age')) 
# get the value assigned to the key gender in my_dict_exp1

print(my_dict_exp1.get('gender')) 
# get the value assigned to the key age then gender in my_dict_exp1
# if the value do not exist return a text 'notfound ha ha ha'

print(my_dict_exp1.get('age','not found ha ha ha')) 
print(my_dict_exp1.get('gender','not found ha ha ha')) 

Accessing Keys keys() - Returns a view of all keys in the dictionary.

print(my_dict_exp1.keys())    

Accessing Values values() - Returns a view of all values in the dictionary.

print(my_dict_exp1.values()) 

Accessing Items keys() - Returns a view of all keys in the dictionary.

print(my_dict_exp1.items())

Modifying Dictionaries

Adding Items

You can add new key-value pairs by simply assigning a value to a new key.

my_dict_exp2['email'] = 'zemzemfiras@GMail.com'
print(my_dict_exp2)

Updating Items

You can change the value of an existing key by assigning a new value.

my_dict_exp2['email'] = 'zemzemfiras@gmail.com'
print(my_dict_exp2)

Now we are only using this exmaple

my_dict_exp2 = dict(name='Firas', work='PhD Student', city='Tunisia', email='zemzemfiras@gmail.com')
print(my_dict_exp2)

Removing Items

Use the del keyword to remove a key-value pair.

del my_dict_exp2['city'] 
print(my_dict_exp2)

use the pop() method to remove and return a value

removed_value = my_dict_exp2.pop('name')  # Removes 'name' and returns 'Alice'
print("removed value is :",removed_value)

Clearing Dictionaries

my_dict_exp1.clear()  # The dictionary is now empty
print(my_dict_exp1)

Nested Dictionaries

A nested dictionary is simply a dictionary that contains other dictionaries as values. This allows for hierarchical data representation, making it useful for complex data structures where values themselves are dictionaries or require grouping into logical structures.

Importance of Nested Dictionaries:

  • Hierarchical Data Representation: They allow the representation of complex data structures, such as organizational structures, configuration settings, and nested objects in APIs.

  • Grouping and Categorizing: Nested dictionaries help to group related data together, which makes it easier to work with and access.

  • Improved Readability: They make code more readable and structured, especially when dealing with multidimensional data, such as JSON-like objects.

  • Efficient Storage and Access: They help in organizing data efficiently, where each dictionary within the main dictionary can hold its own set of key-value pairs.

Basic Example of a Nested Dictionary

A simple case where one dictionary contains another dictionary as a value.

Inside the nested dictionary 'address', we have keys like 'street', 'city', and 'zip'.

person = {
    'name': 'Alice',
    'age': 30,
    'address': {
        'street': '123 Main St',
        'city': 'Wonderland',
        'zip': '12345'
    }
}

# Accessing nested dictionary values
print(person['address']['city'])  # Output: Wonderland

Creating Databases with Nested Dictionaries

an example of storing data about multiple students, where each student has their own dictionary.

students = {
    'student1': {
        'name': 'Alice',
        'age': 20,
        'grades': {
            'math': 90,
            'english': 85
        }
    },
    'student2': {
        'name': 'Bob',
        'age': 22,
        'grades': {
            'math': 80,
            'english': 88
        }
    }
}
print(students)

Accessing a nested Dictionary

# print Student 1 nested Dict

print(students['student1'])
# print Student 2 nested Dict

print(students['student2'])

Accessing a value in nested dictionary

# Accessing Bob's age
print(students['student2']['age'])  

# Accessing the grade of Alice (student1) in Math
print(students['student1']['grades']['math']) 


Configurations with Nested Dictionaries

Nested dictionaries are often used to represent complex configuration files, like settings for a web application.

config = {
    'database': {
        'host': 'localhost',
        'port': 5432,
        'user': 'admin',
        'password': 'securepass'
    },
    'logging': {
        'level': 'DEBUG',
        'log_file': '/var/log/app.log'
    }
}
print(config)

Species Characteristics and Habitats

# Accessing database configurations

print(config['database']['host'])  # Output: localhost
print(config['logging']['log_file'])  # Output: /var/log/app.log

species_data = {
    'lion': {
        'classification': 'Mammal',
        'habitat': {
            'continent': 'Africa',
            'environment': 'Savanna',
            'climate': 'Tropical'
        },
        'diet': 'Carnivore',
        'conservation_status': 'Vulnerable'
    },
    'elephant': {
        'classification': 'Mammal',
        'habitat': {
            'continent': 'Africa',
            'environment': 'Grasslands',
            'climate': 'Tropical'
        },
        'diet': 'Herbivore',
        'conservation_status': 'Endangered'
    },
    'penguin': {
        'classification': 'Bird',
        'habitat': {
            'continent': 'Antarctica',
            'environment': 'Coastal',
            'climate': 'Polar'
        },
        'diet': 'Carnivore',
        'conservation_status': 'Least Concern'
    },
    'kangaroo': {
        'classification': 'Mammal',
        'habitat': {
            'continent': 'Australia',
            'environment': 'Outback',
            'climate': 'Arid'
        },
        'diet': 'Herbivore',
        'conservation_status': 'Near Threatened'
    }
}


print(species_data)
# Accessing specific information:
# 1. Access the habitat of the elephant
elephant_habitat = species_data['elephant']['habitat']
print(f"Elephant Habitat: {elephant_habitat}")
# Output: Elephant Habitat: {'continent': 'Africa', 'environment': 'Grasslands', 'climate': 'Tropical'}

# 2. Access the conservation status of the lion
lion_status = species_data['lion']['conservation_status']
print(f"Lion Conservation Status: {lion_status}")
# Output: Lion Conservation Status: Vulnerable

# 3. Access the diet of the penguin
penguin_diet = species_data['penguin']['diet']
print(f"Penguin Diet: {penguin_diet}")
# Output: Penguin Diet: Carnivore

ENJOY !!

Section 7 : Control flow statements

Control flow statements dictate the order in which instructions in a program are executed. Python provides several mechanisms for controlling flow:

  • Conditional Statements: Execute different blocks of code based on conditions.
  • Loops: Repeat code execution under specified conditions.

Conditional Statements

Conditional statements in Python allow you to execute certain blocks of code based on whether a condition is True or False. These conditions are evaluated using comparison operators or boolean expressions, which can help in making decisions in your code. The main conditional statements in Python are if, if-else, and if-elif-else.

If-Else Statements

If-else statements allow your program to make decisions based on certain conditions.

Syntax

if condition:
    # Code block executed if condition is True
else:
    # Code block executed if condition is False

Basic If-Else Example

x = 10
if x > 5:
    print("x is greater than 5")
else:
    print("x is less than or equal to 5")

Example: Check for a Specific DNA Base

write a Python program to check if a DNA sequence contains the base A (adenine)

dna_sequence = "ATCGGATCGA"

if "A" in dna_sequence:
    print("The sequence contains adenine (A).")
else:
    print("The sequence does not contain adenine (A).")

If-Elif-Else Example:

Sometimes, you need to check multiple conditions. This is where elif (else if) comes into play. It allows you to check multiple conditions in sequence.

if condition1:
    # Code block executed if condition1 is True
elif condition2:
    # Code block executed if condition1 is False and condition2 is True
else:
    # Code block executed if all the above conditions are False

Basic If-Else Example

score = 85

if score >= 90:
    print("Grade: A")
elif score >= 80:
    print("Grade: B")
elif score >= 70:
    print("Grade: C")
else:
    print("Grade: F")

Example: Classify DNA Sequence Based on Length

use if-elif-else to classify DNA sequences based on their length (e.g., short, medium, long sequences).

dna_sequence = "ATGCGTACG"

if len(dna_sequence) < 50:
    print("This is a short DNA sequence.")
elif len(dna_sequence) <= 200:
    print("This is a medium-length DNA sequence.")
else:
    print("This is a long DNA sequence.")

Loops in Python

Loops are used to repeatedly execute a block of code. Python provides two types of loops: for loops and while loops

For Loop

The for loop is generally used when you know the number of iterations you want to perform or when you're working with a sequence of elements (such as a list, string, or tuple).

for variable in sequence:
    # Code block to execute

Basic Example

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(fruit)

Example: Counting Specific Bases in a DNA Sequence

We can use a for loop to iterate over a DNA sequence and count the occurrence of each base

dna_sequence = "ATCGGATCGA"
count_A = 0
count_T = 0
count_C = 0
count_G = 0

for base in dna_sequence:
    if base == "A":
        count_A += 1
    elif base == "T":
        count_T += 1
    elif base == "C":
        count_C += 1
    elif base == "G":
        count_G += 1

print(f"A: {count_A}, T: {count_T}, C: {count_C}, G: {count_G}")

For Loop with Range

Hint : The range() function generates a sequence of numbers, which can be used with for loops.

for i in range(start, stop, step):
    # Code block to execute

example

for i in range(1, 6):  # Range from 1 to 5
    print(i)

example : Extracting Substrings from DNA Sequence

You can use a for loop combined with range() to extract specific sections of a DNA sequence (substrings).

dna_sequence = "ATCGGATCGA"

# Extract every 3rd base from the DNA sequence
for i in range(0, len(dna_sequence), 3):
    print(dna_sequence[i:i+3])  # Extracts a substring of length 3

Nested For Loops

You can have one for loop inside another. This is useful for iterating over multi-dimensional data structures like lists of lists.

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

for row in matrix:
    for element in row:
        print(element)

While Loop

A while loop repeatedly executes a block of code as long as a specified condition is True.

while condition:
    # Code block to execute as long as condition is True

Example

count = 0
while count < 5:
    print(count)
    count += 1  # Increase count by 1 in each iteration

Example: Searching for a Pattern in a DNA Sequence

You can use a while loop when you need to iterate through a sequence and search for specific patterns, such as a certain motif in a DNA sequence.

dna_sequence = "ATCGGATCGAATGATGCTAG"
pattern = "ATG"
index = 0

while index < len(dna_sequence):
    index = dna_sequence.find(pattern, index)
    if index == -1:  # No more occurrences found
        break
    print(f"Pattern '{pattern}' found at index {index}")
    index += len(pattern)  # Move past the found pattern

Break Statements

The break statement is used to exit the current loop (either for or while) prematurely when a specific condition is met. After the break statement is executed, the control moves to the first statement after the loop.

Syntax 1 :

for item in iterable:
    # code block
    if condition_to_break:
        break  # Exit the loop if the condition is met
    # other code

Syntax 2 :

while condition:
    # code block
    if condition_to_break:
        break  # Exit the loop if the condition is met
    # other code

Break in a for loop

The following example finds the first occurrence of the number 5 in a list of numbers and then exits the loop using the break statement.

numbers = [1, 3, 5, 7, 9, 5, 11]
for number in numbers:
    if number == 5:
        print("Found the number 5!")
        break  # Exit the loop after finding the first 5

### Break in a while loop

This example demonstrates the use of break in a while loop, which exits once the variable count exceeds 5.

count = 0
while count <= 5:
    print(count)
    if count == 3:
        break  # Exit the loop when count is 3
    count += 1

Example: Stop Searching for Pattern After a Certain Number of Occurrences

dna_sequence = "ATGCGATGCTGATGGAAT"
pattern = "ATG"
occurrence_limit = 2
count = 0
index = 0

while count < occurrence_limit:
    index = dna_sequence.find(pattern, index)
    if index == -1:  # No more occurrences found
        break
    print(f"Pattern '{pattern}' found at index {index}")
    count += 1
    index += len(pattern)  # Move past the found pattern

Continue Statement

The continue statement is used to skip the current iteration of a loop and move to the next iteration. It is often used when you want to skip certain conditions but continue processing the remaining items.

Syntax1:

while condition:
    # code block
    if condition_to_skip:
        continue  # Skip the rest of the code and move to the next iteration
    # other code

Syntax2:

for item in iterable:
    # code block
    if condition_to_skip:
        continue  # Skip the rest of the code and move to the next iteration
    # other code

Continue in a for loop

The following example skips the number 5 in the list of numbers, but prints the rest.

numbers = [1, 3, 5, 7, 9, 5, 11]
for number in numbers:
    if number == 5:
        continue  # Skip the rest of the loop when number is 5
    print(number)

Continue in a while loop

This example skips the iteration when the variable count is 3, but continues the loop for other values.

count = 0
while count <= 5:
    count += 1
    if count == 3:
        continue  # Skip the iteration when count is 3
    print(count)

Example: Skip a Specific Base in a DNA Sequence

dna_sequence = "ATCGGATCGA"
for base in dna_sequence:
    if base == "G":
        continue  # Skip 'G' bases
    print(base)

Practice

### EXercice 1 : Write a program that checks if a given DNA sequence contains both "ATG" (start codon) and "TAA" (stop codon).

dna_sequence = input("Enter DNA sequence: ")

if "ATG" in dna_sequence and ("UAG" in dna_sequence or "UAA" in dna_sequence or "UGA" in dna_sequence):
    print("The sequence contains both the start and stop codons.")
else:
    print("The sequence does not contain both start and stop codons.")

Exercice 2 :

Write a program to count how many times the codon "ATG" appears in a DNA sequence.

dna_sequence = "ATGCGATGCTGATGGAAT"
count_ATG = 0
#print(len(dna_sequence))
#print(range(len(dna_sequence) - 2))

for i in range(len(dna_sequence) - 2):  # Loop through the sequence
    #print(i)
    #print(dna_sequence[i:(i+3)])
    if dna_sequence[i:i+3] == "ATG":
        count_ATG += 1

print(f"Codon 'ATG' appears {count_ATG} times.")

Exercice 3 :

Write a program that continuously asks the user for DNA sequences and checks if the sequence is valid (only contains A, T, C, G) until a valid sequence is entered.

valid_bases = set("ATCG")
while True:
    dna_sequence = input("Enter a DNA sequence: ")
    if set(dna_sequence.upper()).issubset(valid_bases):
        print("Valid DNA sequence.")
        break
    else:
        print("Invalid DNA sequence. Please enter only A, T, C, and G.")

Exercice 4 :

Write a program that stops when it finds the first "CG" pair in a DNA sequence and skips all other "CG" pairs.

dna_sequence = "ATGCGATGCGCGT"
index = 0

while index < len(dna_sequence):
    index = dna_sequence.find("CG", index)
    if index == -1:  # No more occurrences found
        break
    print(f"'CG' found at index {index}")
    index += 2  # Move past the found "CG"


Enjoy

Section 8 : Functions

A function is a block of reusable code that performs a specific task. Functions allow us to break down complex problems into smaller, more manageable parts. Python provides a simple way to define and use functions.

In this lesson, we will cover:

- Defining a function
- Function parameters
- Return values
- Function scope
- Lambda functions
- Recursive functions

Defining a Function

To define a function in Python, we use the def keyword followed by the function name, parentheses (), and a colon :. After the colon, we write the code block that should execute when the function is called.

Syntax:

def function_name():
    # Code to be executed
    print("Hello, this is a function!")

Example

# Make a fuction that prints "Hello Learners" 

def Greeting(): 
    print("Hello Learners")


# Call Greeting() function
Greeting()

Functions Parameters

Functions can accept inputs, known as parameters. These parameters are specified within the parentheses of the function definition.

Syntax:

def function_name(parameter1, parameter2):
    # Code that uses the parameters
    print(f"Hello {parameter1}, you are {parameter2} years old.")

Example

# function name : introduce 
# Parameter  1  : name 
# Parameter  2  : age
def introduce(name, age):
    print(f"Hello {name}, you are {age} years old.")

# Calling the function with arguments alice for name / 25 for age
introduce("Alice", 25)  

Return Values

A function can return a value using the return keyword. This value can then be used elsewhere in the program.

Syntax:

def function_name():
    return value

Example

def add_numbers(a, b):
    return a + b

result = add_numbers(10, 5)
print(f"The sum is: {result}")

Local Scope

Variables created inside a function are local to that function and cannot be accessed outside of it. This is called local scope. Variables defined outside the function are considered global variables and can be accessed anywhere in the program.

Example

def show_local_scope():
    local_variable = "I am local"
    print(local_variable)

show_local_scope()

# Uncommenting the next line will cause an error
# print(local_variable)  # This will give an error because local_variable is not accessible outside the function

lets try to print local variable outside

def show_local_scope():
    local_variable = "I am local"



show_local_scope()

# Uncommenting the next line will cause an error
# print(local_variable)  # This will give an error because local_variable is not accessible outside the function

lets make a global variable : global_variable then implement it in our function

global_variable  = "I am global"


def show_local_scope():
    print(global_variable)


show_local_scope()

Default Parameters

You can assign default values to parameters in a function. This way, if no argument is passed when calling the function, the default value will be used.

Syntax:

def function_name(parameter1=default_value):
    # Code using the parameter
    print(parameter1)

Example:

def greet(name="Guest"):
    print(f"Hello, {name}!")

greet("Alice")  
greet()         

Lambda Functions

A lambda function is a small anonymous function defined using the lambda keyword. These functions are typically used for short, one-off operations.

Syntax:

lambda arguments: expression
multiply = lambda x, y: x * y
print(multiply(4, 5))  # Output: 20

Recursive Functions

A recursive function is one that calls itself in its definition. It is often used to solve problems that can be broken down into smaller, similar subproblems (e.g., calculating factorials, traversing trees).

Syntax:

def recursive_function():
    # base case
    if condition:
        return result
    # recursive case
    else:
        return recursive_function()

Example

def factorial(n):
    # Base case: factorial of 0 or 1 is 1
    if n == 0 or n == 1:
        return 1
    # Recursive case
    else:
        return n * factorial(n - 1)

print(factorial(5))  

Functions as Arguments

You can pass functions as arguments to other functions. This allows for greater flexibility and allows for higher-order functions.

Example:

def apply_function(f, x):
    return f(x)

def square(n):
    return n * n

result = apply_function(square, 5)
print(result)  # Output: 25

Appling Functions in Bioinformatics

Example: Calculating GC Content

This function calculates the GC content of a DNA sequence.

  • GC content is the percentage of G and C bases in the sequence.

  • Args:

    • sequence (str): A string representing the DNA sequence (e.g., 'ATGCGT').

    • Returns: float: The GC content as a percentage.

def gc_content(sequence):
    
    gc_count = sequence.count('G') + sequence.count('C')
    return (gc_count / len(sequence)) * 100

sequence = "ATGCGTACG"
gc_percentage = gc_content(sequence)
print(f"GC content: {gc_percentage:.2f}%")

Example: Counting Nucleotides in a DNA Sequence

This function counts the occurrences of each nucleotide in a DNA sequence.

  • Args:

    • sequence (str): A string representing the DNA sequence (e.g., 'ATGCGT').

    • Returns: dict: A dictionary with counts for 'A', 'T', 'C', and 'G'.

def count_nucleotides(sequence):
    return {'A': sequence.count('A'),
            'T': sequence.count('T'),
            'C': sequence.count('C'),
            'G': sequence.count('G')}

sequence = "ATGCGTACG"
nucleotide_counts = count_nucleotides(sequence)
print(f"Nucleotide counts: {nucleotide_counts}")

Example: Finding a Subsequence in a DNA Sequence

This function checks if a given subsequence is found in a DNA sequence.

  • Args:

    • sequence (str): The DNA sequence to search within.

    • subsequence (str): The subsequence to search for.

  • Returns: bool: True if subsequence is found, False otherwise.

def find_subsequence(sequence, subsequence):
    if subsequence in sequence:
        return True
    else:
        return False

sequence = "ATGCGTACG"
subsequence = "CGT"
found = find_subsequence(sequence, subsequence)
print(f"Subsequence found: {found}")

Example: Translating DNA to Protein This function translates a DNA sequence into a protein sequence. It translates each codon (3 nucleotides) into an amino acid.

  • Args:

    • dna_sequence (str): The DNA sequence to translate.

    • Returns: str: The corresponding protein sequence.

def translate_dna_to_protein(dna_sequence):
    codon_table = {
        "ATA": "I", "ATC": "I", "ATT": "I", "ATG": "M",
        "ACA": "T", "ACC": "T", "ACG": "T", "ACT": "T",
        "AAC": "N", "AAT": "N", "AAA": "K", "AAG": "K",
        "AGC": "S", "AGT": "S", "AGA": "R", "AGG": "R",
        "CTA": "L", "CTC": "L", "CTG": "L", "CTT": "L",
        "CCA": "P", "CCC": "P", "CCG": "P", "CCT": "P",
        "CAC": "H", "CAT": "H", "CAA": "Q", "CAG": "Q",
        "CGA": "R", "CGC": "R", "CGG": "R", "CGT": "R",
        "GTA": "V", "GTC": "V", "GTG": "V", "GTT": "V",
        "GCA": "A", "GCC": "A", "GCG": "A", "GCT": "A",
        "GAC": "D", "GAT": "D", "GAA": "E", "GAG": "E",
        "GGA": "G", "GGC": "G", "GGG": "G", "GGT": "G",
        "TCA": "S", "TCC": "S", "TCG": "S", "TCT": "S",
        "TTC": "F", "TTT": "F", "TTA": "L", "TTG": "L",
        "TAC": "Y", "TAT": "Y", "TAA": "*", "TAG": "*",
        "TGC": "C", "TGT": "C", "TGA": "*", "TGG": "W",
        "CTA": "L", "CTC": "L", "CTG": "L", "CTT": "L",
    }
    
    protein = ""
    # Iterate over the sequence in steps of 3 nucleotides (codons)
    for i in range(0, len(dna_sequence), 3):
        codon = dna_sequence[i:i+3]
        if codon in codon_table:
            protein += codon_table[codon]
    
    return protein

dna_sequence = "ATGGCCAAGGTTTAA"
protein_sequence = translate_dna_to_protein(dna_sequence)
print(f"Protein sequence: {protein_sequence}")


ENJOY !!

Section 9 : Error Handling

Error handling is a critical part of writing reliable Python code. In this course, we'll dive deep into Python's error-handling mechanism, starting from the basics and moving towards advanced topics like custom exceptions, context managers, and logging errors. By the end of this course, you'll have a solid understanding of how to manage errors effectively in your Python programs.

Basics of Error Handling

What are Errors and Exceptions?

  • Errors: Critical problems that are typically not recoverable (e.g., syntax errors, system-level issues).

  • Exceptions: Conditions that arise during the execution of a program but can be handled and recovered from.

Try-Except Block

The try-except block is the fundamental building block of error handling in Python. It allows you to catch and handle exceptions that occur during the execution of a program. By using try-except, you can prevent your program from crashing and provide meaningful responses to errors.

Why Use try-except?

  • Prevent your program from crashing due to runtime errors.
  • Gracefully handle unexpected situations and provide meaningful error messages.
  • Allow execution of alternative logic when errors occur.

Syntax:

try:
    # Code that might raise an exception
except ExceptionType:
    # Code to handle the exception

Lets try to print this code :

print(10 / 0)

By using try-except, you can prevent your program from crashing and provide meaningful responses to errors.

try:
    result = 10 / 0
except:
    print("An error occurred.") # if it causes an error print "An error occurred."

Using the Exception Object

To access details about the exception, use the as keyword.

try:
    x = 10 / 0                    # will crach and provide an error
except ZeroDivisionError as e:    # if it is an error print details about it without crashing.
    print(f"Error occurred: {e}")

Multiple except Blocks

You can use multiple except blocks to handle different types of exceptions.


try:
    x = int("hello")  # Will raise a ValueError
except ZeroDivisionError:
    print("Division by zero error")
except ValueError:
    print("Invalid input: Could not convert to an integer")

Generic Exception Handling

To handle any exception, use except Exception. While it's better to catch specific exceptions, this can be useful for debugging.

Syntax:

try:
      # Code that might raise an exception
except Exception as e:
    print(f"An unexpected error occurred: {e}")

Example 1

try:
    result = 10 / "a"  # TypeError
except Exception as e:
    print(f"An unexpected error occurred: {e}")

Example 2: Reading a File Safely

try:
    with open("file.txt", "r") as file:
        data = file.read()
except FileNotFoundError:
    print("Error: The file does not exist.")
except PermissionError:
    print("Error: You do not have permission to access this file.")

or making it more general

try:
    with open("file.txt", "r") as file:
        data = file.read()
except Exception as e:
    print(f"Error: {e}")

Else and Finally

The else and finally clauses add more control and flexibility to Python's try-except mechanism, allowing you to handle code execution in different scenarios, such as when no exception occurs or when cleanup operations are needed.

Else Clause

The else block is executed only if no exception occurs in the try block. This is useful for separating error handling (except) from normal execution when everything works as expected.

  • If an exception occurs: The else block is skipped.
  • If no exception occurs: The else block is executed.

Syntax

try:
    # Code that might raise an exception
except ExceptionType:
    # Code to handle the exception
else:
    # Code to execute if no exception occurred
try:
    result = 10 / 2 # No exception occurs
except ZeroDivisionError:
    print("Error: Division by zero.")
else:
    print(f"Division successful, result is {result}")

finally Clause

The finally block is executed no matter what, whether an exception is raised or not. This is useful for cleanup actions, like closing a file or releasing a resource.

Syntax

try:
    # Code that might raise an exception
except ExceptionType:
    # Code to handle the exception
finally:
    # Code to execute regardless of what happens

Example

try:
    x = int(input("Enter the numerator: "))
    y = int(input("Enter the denominator: "))
    div = x / y  # Perform division
except ValueError as e:
    print(f"Error: Invalid input. Please enter integers only. {e}")
except ZeroDivisionError as e:
    print(f"Error: Cannot divide by zero. {e}")
else:
    print(f"Division was successful: {x}/{y} = {div}")
finally:
    print("This will always execute.")

raise Statement in Python

The raise statement in Python is used to explicitly raise an exception. It allows you to signal that something unexpected has occurred and gives you control over the error being raised.

  • ExceptionType: The type of exception you want to raise (e.g., ValueError, TypeError, KeyError, or a custom exception).
  • Error message: An optional message providing more details about the error.

Syntax

raise ExceptionType("Error message")

Example 1 : Input Validation

def validate_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    print(f"Valid age: {age}")

# Test the function
try:
    validate_age(-5)
except ValueError as e:
    print(f"Error: {e}")

Example 2: Raising TypeError

def add_numbers(a, b):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("Both arguments must be numbers")
    return a + b

# Test the function
try:
    result = add_numbers(10, "20")
except TypeError as e:
    print(f"Error: {e}")

Reraising an Exception

If you want to re-raise an exception that you've caught, you can use raise without specifying an exception. This is useful for adding context to an error before passing it on.

try:
    x = 10 / 0
except ZeroDivisionError:
    print("Handling ZeroDivisionError")
    raise

Using raise with a Cause

The raise statement can be used with a from keyword to link a new exception to an original one. This is helpful for debugging complex issues.

try:
    int("abc")  # Raises ValueError
except ValueError as e:
    raise TypeError("Conversion error occurred") from e

Using Assert for Debugging

The assert statement is a debugging tool in Python used to test assumptions in your code. It helps ensure that a condition is true at a specific point in the program. If the condition evaluates to False, assert raises an AssertionError and optionally displays an error message.

Syntax

    assert condition, "Error message (optional)"
  • condition: A boolean expression that you expect to be True.
  • "Error message": Optional; describes why the assertion failed.

If the condition evaluates to False:

  • An AssertionError is raised.
  • The optional error message (if provided) is displayed.

When to Use assert

  • To test assumptions in your code while developing or debugging.
  • To ensure inputs to a function meet certain criteria.
  • To check the state of variables at critical points in your code.

Example

x = 10
assert x > 0, "x must be positive"
print("Assertion passed!")

Try a negative x

x = -5
assert x > 0, "x must be positive"
print("Assertion passed!")

Example 2 :

assert 2 + 2 == 4, "Math error"
assert 2 + 2 == 5, "Math error"  # This will raise AssertionError

Validating Function Input

def divide(a, b):
    assert b != 0, "Denominator cannot be zero"
    return a / b

# Test the function
print(divide(10, 2))  # Valid
print(divide(10, 0))  # Raises AssertionError

Example 3 : Checking Data Integrity

data = [1, 2, 3, 4]
assert all(isinstance(x, int) for x in data), "All elements in the list must be integers"
print("Data is valid!")

Handling Signals and External Errors

Signals are a mechanism used by operating systems to notify a process of an event. Python's signal module allows you to handle these signals gracefully.

Common Use Cases for Signals

  • Handling Ctrl+C (keyboard interrupt) gracefully.
  • Responding to timeouts or resource limits.
  • Coordinating between processes in multiprocessing or parallel programs. Note : This part will be developped in another part.

ENJOY !!

Section 10 : Class

what is it !!!

A class is a blueprint for creating objects. Objects are instances of a class, and they bundle together data (attributes) and behavior (methods) to model real-world entities.

Syntax

class ClassName:
    # Attributes and methods go here

## Class's Components

Constructor (init)

The constructor is a special method that is called automatically when an object is created. It initializes the object’s attributes.

class MyClass: 
    def __init__(self, name, age):
        self.name = name  # Assigning the value to self.name
        self.age = age    # Assigning the value to self.age

    def greeting(self):
        return f"Hello, {self.name}"


# Instantiate the class with the correct arguments
person1 = MyClass("Mario", 30)

print(person1.name)
print(person1.age)

# Call the greet method to see the result
print(person1.greeting())


Atributes

Attributes in a class are variables or data that are associated with a class and its objects. They are used to store information relevant to the class and its instances. Attributes can be broadly categorized as:

  • Instance Attributes: Unique to each object and set in the constructor.

  • Class Attributes: Shared by all objects of the class.

class MyClass:
    class_attribute = "Shared by all instances"  # Class attribute

    def __init__(self, name):
        self.name = name  # Instance attribute

Class Attribute:

  • Class attributes are accessed through the class itself or through any instance. However, modifying a class attribute through an instance will modify it only for that specific instance, not the class itself.

    • These are attributes that are shared across all instances of a class.

    • They are defined within the class but outside any methods.

    • Class attributes are accessed using the class name or an instance of the class.

class MyClass:
    class_attribute = "Shared by all instances"  # Class attribute

    def __init__(self, name):
        self.name = name  # Instance attribute

# Creating instances
obj1 = MyClass("Alice")
obj2 = MyClass("Bob")

# Accessing the class attribute via the class
print(MyClass.class_attribute)  # Shared by all instances

# Accessing the class attribute via an instance
print(obj1.class_attribute)  # Shared by all instances
print(obj2.class_attribute)  # Shared by all instances

Instance Attribute:

  • Instance attributes are unique to each object created from the class and can only be accessed through the instance.

    • These are attributes specific to an instance of a class.

    • They are typically defined in the init method and are prefixed with self.

    • Each instance of the class has its own copy of the instance attributes.

# Accessing the instance attribute
print(obj1.name)  # Alice
print(obj2.name)  # Bob
# Modifying the class attribute via an instance
obj1.class_attribute = "Modified by obj1"
print(obj1.class_attribute)  # Modified by obj1 (specific to obj1)
print(obj2.class_attribute)  # Shared by all instances (not modified for obj2)

Creating an Object

Syntax

Object = Class_Name(Attributes)
# Define a class
class Person:
    def __init__(self, name, age):
        self.name = name  # Instance attribute
        self.age = age    # Instance attribute

    def introduce(self):  # Instance method
        return f"My name is {self.name}, and I am {self.age} years old."

# Create an object (instance of Person)
person1 = Person("BatMan", 25)


# Access attributes
print(person1.name)  # Output: BatMAn
print(person1.age)   # Output: 25
# Call a method
print(person1.introduce())  # Output: My name is Alice, and I am 25 years old.

Methods

Methods are functions defined within a class. They define the behavior of objects. Methods take self as the first argument to access object attributes.

Instance Methods:

Work with an instance of the class (self refers to the instance).
class MyClass:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, {self.name}!"

print(MyClass("SuperMan").greet())

# or Object = Class_Name(Attributes)
person = MyClass("BatMan")
print(person.greet())

Class Methods:

Work on the class itself. Use @classmethod and the cls parameter.
class MyClass:
    class_attribute = "Hello, World!"

    @classmethod
    def class_method(cls, param):
        print(f"Class Attribute: {cls.class_attribute}")
        print(f"Parameter: {param}")

# Call the class method
MyClass.class_method("Example")


Static Methods:

Do not depend on instance or class. Use @staticmethod.
class Example:
    class_attribute = "Shared"

    def __init__(self, value):
        self.instance_attribute = value

    @classmethod
    def class_method(cls):
        return f"Class Attribute: {cls.class_attribute}"

    @staticmethod
    def static_method():
        return "This is a static method"

# Usage
obj = Example("Unique Value")
print(obj.class_method())  # Output: Class Attribute: Shared
print(obj.static_method())  # Output: This is a static method

Inheritance

Inheritance allows a class (child class) to inherit attributes and methods from another class (parent class).

# Parent class
class Animal:
    def __init__(self, species):
        self.species = species

    def make_sound(self):
        return "Some generic sound"

# Child class
class Dog(Animal):
    def make_sound(self):
        return "Woof!"

# Create an instance of Dog
dog = Dog("Canine")
print(dog.species)  # Output: Canine
print(dog.make_sound())  # Output: Woof!

Encapsulation

Encapsulation restricts access to some attributes or methods to ensure they are not modified directly.

  • Use a single underscore (_) for protected attributes.

  • Use a double underscore (__) for private attributes.

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private attribute

    def deposit(self, amount):
        self.__balance += amount

    def get_balance(self):
        return self.__balance

# Create an account
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500

Polymorphism

  • Polymorphism allows methods to have the same name but behave differently based on the object.
class Cat:
    def make_sound(self):
        return "Meow"

class Dog:
    def make_sound(self):
        return "Woof"

animals = [Cat(), Dog()]
for animal in animals:
    print(animal.make_sound())  # Output: Meow, Woof

Example

class Car:
    # Class attribute
    wheels = 4

    # Constructor
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year

    # Instance method
    def display_details(self):
        return f"{self.year} {self.brand} {self.model}"

# Create instances
car1 = Car("Toyota", "Camry", 2020)
car2 = Car("Honda", "Civic", 2018)

# Access attributes and methods
print(car1.display_details())  # Output: 2020 Toyota Camry
print(car2.display_details())  # Output: 2018 Honda Civic

Summary of Key Terms

TermDescription
ClassA blueprint for creating objects.
ObjectAn instance of a class.
AttributeA variable that holds data for the object.
MethodA function defined in a class that operates on objects.
ConstructorThe __init__ method used to initialize object attributes.
InheritanceA way to create a new class using an existing class as a base.
EncapsulationRestricting access to certain attributes or methods.
PolymorphismAllowing methods to have the same name but behave differently in different contexts.

ENJOY !!

Section 11 : File Handling

File handling is an essential part of programming that involves creating, reading, writing, and manipulating files. Python provides built-in functions and modules to perform file handling operations efficiently.

Checking File Existence

Before performing file operations, it’s good practice to check if a file exists.

Using os Module

import os 

if os.path.exists('README.md'):
    print('File exists... ')
else: print('File Not Found')

Using os Module

from pathlib import Path

myfile = Path('README.md')

if myfile.exists(): 
    print('File exists... ')
else: print('File Not Found')

Exception Handling

try:
    with open('exercices.txt', 'r') as file:
        print(file.read())
except FileNotFoundError:
    print('File not found.')
except IOError as e:
    print(f'An error occurred: {e}')

Opening and Closing Files

To open a file python uses the open() fuction .

Syntax

file = open('filename', mode)
  • filename :Name of the file to be opened.

  • mode : Specifies the mode in which the file is opened (e.g., read, write).

    • Common file modes:

      • 'r' : Read (default mode). File must exist.

      • 'w' : Write. Creates a new file or truncates an existing one.

      • 'x' : Create. Fails if the file exists.

      • 'a' : Append. Adds to the end of the file.

      • 'b' : Binary mode.

      • 't' : Text mode (default).

      • '+' : Open for both reading and writing.

Method 1

# open file in read mode 'r'
file = open("shortstory.txt","r")
# Read file content
file.read()

if we add "file.close()" to the above cell the file will be closed

# open file in read mode 'r'
file = open("shortstory.txt","r")
# Read file content 
file.read()
file.close()

Note that printing content file is different to read mode as we could print a file content of closed file

# open file in read mode 'r'
file = open("shortstory.txt","r")
# Read file content 
print(file.read())
file.close()

Method 2 : Using with Statement (Preferred Method)

The with statement ensures the file is closed automatically:

with open('shortstory.txt', 'r') as file: 
    content = file.read()
    print(content) # File is closed after this block

Reading Files

Python provides multiple methods to read files based on requirements...

Reading the Entire File

Reads the entire file content as a string

with open('shortstory.txt', 'r') as file:
    content = file.read()
    print(content)


Read Line by Line

Useful for processing files line by line

with open('shortstory.txt', 'r') as file:
    for line in file:
        print(line)  # Removes newline character \n


Returns all lines as a list of strings

with open('shortstory.txt', 'r') as file:
    lines = file.readlines()
    print(lines)


Read Fixed Number of Characters

with open('shortstory.txt', 'r') as file:
    content = file.read(110)  # Reads the first 110 characters
    print(content)

Writing to Files

Writing New Content

# as we dont have a practiceFile.txt in our directory 
# 'w' will automatically create a new file 

with open('practiceFile.txt', 'w') as file:
    file.write("this is a new line ")    
with open('practiceFile.txt', 'r') as file:
    print(file.read())

Write another line : "Date: coming soon"

with open('practiceFile.txt', 'w') as file:
    file.write("Title : Nextflow Course")
    
with open('practiceFile.txt', 'r') as file:
    print(file.read())
    

Appending Content

To avoid overwriting in your file, and append content use 'a' mode and the newline character ( \n ) (optional)

with open('practiceFile.txt', 'a') as file:
    file.write('\nDate: coming soon.')

with open('practiceFile.txt', 'r') as file:
    print(file.read())
    

Writing Multiple Lines

lines = ['\nFiras Zemzem ', '\nzemzemfiras@gmail.com \n']
with open('practiceFile.txt', 'a') as file:
    file.writelines(lines)  # Writes a list of strings

with open('practiceFile.txt', 'r') as file:
    print(file.read())
    

## File Positioning

Python provides methods to manipulate the file pointer during reading or writing

file.seek() : Moves the file pointer!

- 0 (default): Start of the file.

- 1: Current position.

- 2: End of the file.
# Corrected code
with open('practiceFile.txt', 'a+') as file: # Allows both writing(appending) and reading
    file.write("Tunisia")
    file.seek(0)  # Move pointer to the start of the file
    print(file.read())  # Read the content to verify

File Copying

Copy the contents of one file to another

with open('practiceFile.txt', 'r') as src, open('ContactINFO.txt', 'w') as dest:
    dest.write(src.read())

CSV File Handling

Loading the Dataset

Using csv.reader

when you want to iterate over rows as lists.

import csv

with open('employee_records.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)

Using csv.DictReader

csv.DictReader reads the file into dictionaries where the keys are the column headers.

import csv

with open('employee_records.csv', 'r') as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(row)


csv.DictReader is particularly useful if you want to access specific fields by their names.

with open('employee_records.csv', 'r') as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(f"Name: {row['Name']}, \t Department: {row['Department']}, \t Salary : {row['Salary']}")

Add lines

import csv

# Add a new row to the CSV file
new_row = [9, 'Bob Brown', 'Engineering', 70000, '2024-01-15']

# Open the file in append mode ('a') to add a new row
with open('employee_records.csv', 'a', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(new_row)

# Read and print the content of the CSV file
with open('employee_records.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)

Filtering rows based on conditions

import csv

with open('employee_records.csv', 'r') as file:
    reader = csv.DictReader(file)
    for row in reader:
        if row['Department'] == 'Engineering':
            print(row)

Sorting and grouping data

import csv

with open('employee_records.csv', 'r') as infile, open('sorted_employees.csv', 'w', newline='') as outfile:
    reader = csv.DictReader(infile)
    sorted_data = sorted(reader, key=lambda x: int(x['Salary']), reverse=True)

    writer = csv.DictWriter(outfile, fieldnames=reader.fieldnames)
    writer.writeheader()
    writer.writerows(sorted_data)

Check for Missing Data

import csv

# Open the CSV file for reading
with open('employee_records.csv', 'r') as file:
    reader = csv.reader(file)
    
    # Track if any missing data is found
    missing_data_found = False

    # Check each row for missing data
    # Iterates through the rows in the CSV file, 
    # starting the row numbering from 1 (useful for reporting row numbers).

    for row_number, row in enumerate(reader, start=1):
       # print (row_number)
        
        if any(field.strip() == '' for field in row):  # Check for empty or whitespace-only fields
           #print (row_number)
            print(f"Missing data found in row {row_number}: {row}")
            missing_data_found = True

    if not missing_data_found:
        print("No missing data found in the CSV file.")


ENJOY !!