Intro

Python is a versatile and powerful language that plays a key role in automation, development, and data management. This refresher covers essential Python concepts, making it perfect for anyone looking to strengthen their foundation and boost productivity across various domains.

Comments

Types of Comments in Python

  1. Single-Line Comments: Single-line comments begin with the # symbol. Everything after the # on the same line is ignored by Python.

    1
    2
    
    # This is a single-line comment
    print("Hello, World!")  # This is an inline comment
    
  2. Multi-Line Comments: For multi line comments you can use multiple # symbols or triple quotes (’’’ or “”").

    1
    2
    3
    4
    5
    6
    7
    8
    
    # This is a multi-line comment
    # Line 1
    # Line 2
    
    """
    This is a multi-line comment
    explaining the purpose of the program.
    """
    
  3. Docstrings: Docstrings (short for documentation strings) are a type of comment used to document modules, classes, methods, or functions. They are written using triple quotes (’’’ or “”").

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    def greet(name):
        """
        This function greets a person by their name.
        Arguments:
        name: str - The name of the person to greet
        """
        print(f"Hello, {name}!")
    
    print(greet.__doc__) # Access Docstring programmatically using the __doc__ attribute.
    

Data types

Python offers a diverse range of data types to efficiently handle various kinds of data. These data types are broadly categorized into primitive types and collection.

As a dynamically typed language, Python does not require explicit data type declarations when defining variables. Instead, the data type of a variable is automatically inferred based on the value assigned to it, enabling more flexible and concise code.

  1. Primitive Data Types: Primitive data types represent single values and are the building blocks of Python.

    • Numeric Types:
      1. Integer (int): Represents whole numbers
        1
        2
        
        num = 42
        print(type(num))  # Output: <class 'int'>
        
      2. Float (float): Represents decimal numbers
        1
        2
        
        price = 19.99
        print(type(price))  # Output: <class 'float'>
        
      3. Complex (complex): Represents complex numbers (real and imaginary parts).
        1
        2
        
        z = 3 + 4j
        print(type(z))  # Output: <class 'complex'>
        
    • Boolean (bool): Represents True or False values.
      1
      2
      
      is_active = True
      print(type(is_active))  # Output: <class 'bool'>
      
    • String (str): Represents text data.
      1
      2
      
      name = "Alice"
      print(type(name))  # Output: <class 'str'>
      
  2. Collection Data Types: Collection types store multiple items in a single variable.

    • List (list): A mutable, ordered collection of items. Allows duplicate values, supports indexing and slicing.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      fruits = ["apple", "banana", "cherry"]
      print(type(fruits))  # Output: <class 'list'>
      print(fruits[0])  # Access the element at index 0,  Output: apple
      print(fruits[0:1]) # Slicing, Access elements from index 0 to 1, Output: [apple, banana]
      fruits.append("guava") # Add new element
      fruits.insert("grapes") # Add new element
      fruits.remove() # Remove element
      fruits.pop() # Remove element
      fruits[1] = "custard-apple" # Modifying elements
      
    • Tuple (tuple): An immutable, ordered collection of items (faster than lists.).
      1
      2
      
      coordinates = (10, 20)
      print(type(coordinates))  # Output: <class 'tuple'>
      
    • Set (set): An unordered collection of unique items. No duplicate values, supports mathematical operations like union and intersection.
      1
      2
      
      unique_numbers = {1, 2, 3, 3}
      print(unique_numbers)  # Output: {1, 2, 3}
      
    • Dictionary (dict): A collection of key-value pairs. Keys must be unique, values can be of any type.
      1
      2
      3
      4
      5
      
      user = {"name": "Alice", "age": 25}
      print(type(user))  # Output: <class 'dict'>
      print(user[name])  # Output: Alice
      user["gender"] = "female"   # set new key-value pair
      del user["age"]  # remove key-value pair
      
  3. Specialized Data Types: Python also supports specialized data types for specific use cases.

    • NoneType (None): Represents the absence of a value.

      1
      2
      
      result = None
      print(type(result))  # Output: <class 'NoneType'>
      
    • Bytes (bytes): Represents immutable sequences of bytes (used for binary data).

      1
      2
      
      data = b"hello"
      print(type(data))  # Output: <class 'bytes'>
      
    • Bytearray (bytearray): A mutable sequence of bytes.

      1
      2
      
      data = bytearray(5)  # Creates a bytearray of size 5
      print(type(data))  # Output: <class 'bytearray'>
      
  4. Type Conversion: Python allows you to convert data types using built-in functions.

    • Implicit Conversion: Python automatically converts types where safe.

      1
      2
      
      result = 5 + 2.5  # int + float = float
      print(result, type(result))  # Output: 7.5 <class 'float'>
      
    • Explicit Conversion: Use functions like int(), float(), str() to convert types.

      1
      2
      
      num = "123"
      print(int(num))  # Output: 123
      

Operators

Operators are special symbols or keywords in Python that perform specific operations on variables and values.

  1. Arithmetic Operators: Perform mathematical operations like addition, subtraction, multiplication, etc.

    OperatorDescriptionExampleOutput
    +Addition5 + 38
    -Subtraction5 - 32
    *Multiplication5 * 315
    /Division5 / 22.5
    //Floor Division5 // 22
    %Modulus (remainder)5 % 21
    **Exponentiation2 ** 38
  2. Comparison (Relational) Operators: Compares two values and return a boolean result (True or False).

    OperatorDescriptionExampleOutput
    ==Equal to5 == 5True
    !=Not equal to5 != 3True
    >Greater than5 > 3True
    <Less than5 < 3False
    >=Greater than or equal to5 >= 3True
    <=Less than or equal to5 <= 3False
  3. Logical Operators: Combines conditional statements and return a boolean result.

    OperatorDescriptionExampleOutput
    andLogical ANDTrue and FalseFalse
    orLogical ORTrue or FalseTrue
    notLogical NOTnot TrueFalse
  4. Assignment Operators: Assignment operators are used to assign values to variables, with optional arithmetic or bitwise operations.

    OperatorDescriptionExampleEquivalent To
    =Assign valuex = 5
    +=Add and assignx += 3x = x + 3
    -=Subtract and assignx -= 3x = x - 3
    *=Multiply and assignx *= 3x = x * 3
    /=Divide and assignx /= 3x = x / 3
    //=Floor divide and assignx //= 3x = x // 3
    %=Modulus and assignx %= 3x = x % 3
    **=Exponent and assignx **= 3x = x ** 3
  5. Bitwise Operators: Works on binary representations of numbers.

    OperatorDescriptionExampleOutput
    &Bitwise AND5 & 31
    |Bitwise OR5 | 37
    ^Bitwise XOR5 ^ 36
    ~Bitwise NOT~5-6
    «Left Shift5 « 110
    »Right Shift5 » 12
  6. Membership Operators: Checks if a value exists in a sequence.

    OperatorDescriptionExampleOutput
    inValue exists in sequence“a” in “apple”True
    not inValue does not exist in sequence“b” not in “apple”True
  7. Identity Operators: Checks whether two variables refer to the same object in memory.

    OperatorDescriptionExampleOutput
    isSame objecta is bTrue
    is notDifferent objectsa is not bFalse

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Arithmetic
x = 10
y = 3
print(x + y)  # Addition: 13
print(x % y)  # Modulus: 1

# Comparison
print(x > y)  # True

# Logical
print(x > 5 and y < 5)  # True

# Membership
fruits = ["apple", "banana", "cherry"]
print("apple" in fruits)  # True

# Identity
a = [1, 2, 3]
b = a
c = a[:]
print(a is b)       # True (same object)
print(a is not c)   # True (different objects)

Conditionals

Python uses the if, elif, and else keywords to create conditional logic. The syntax is simple and readable:

Syntax:

1
2
3
4
5
6
if condition:
    # code block to execute if condition is True
elif another_condition:
    # code block to execute if another_condition is True
else:
    # code block to execute if no conditions are True

Example:

1
2
3
4
5
6
7
8
temperature = 30
if temperature > 35:
    print("It's very hot outside!")
elif temperature > 20:
    print("It's warm outside!")
else:
    print("It's cold outside!")
# Output: It's warm outside!

Loops

Loops are an essential construct in Python that allow you to execute a block of code repeatedly. Python provides two main types of loops: for and while.

for loop: The for loop in Python is used to iterate over a sequence (like a list, tuple, string, or range). It allows you to perform an action for every item in the sequence.

Syntax:

1
2
for item in sequence:
    # Code block to execute

Example:

1
2
3
4
5
6
7
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
    print(fruit)
# Output:
# apple
# banana
# cherry

while loop: The while loop continues to execute a block of code as long as its condition is True.

Syntax:

1
2
while condition:
    # Code block to execute

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
count = 5
while count > 0:
    print(count)
    count -= 1
# Output:
# 5
# 4
# 3
# 2
# 1

Functions

Functions are a fundamental building block in Python, allowing you to encapsulate reusable blocks of code. They enhance modularity, readability, and maintainability by enabling you to organize your code into meaningful units.

Example:

1
2
3
4
5
def greet(name):
    """Greet a person with their name."""
    return f"Hello, {name}!"

print(greet("Alice"))

Function Parameters:

  1. Positional Arguments: Arguments passed in the order defined.
  2. Keyword Arguments: Arguments passed with parameter names for clarity.
  3. Default Parameters: Provide default values to parameters.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def describe_pet(pet_name, animal_type="dog"):
    """Describe a pet."""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name}.")

# Positional arguments
describe_pet("Buddy", "cat")

# Keyword arguments
describe_pet(animal_type="hamster", pet_name="Hammy")

# Default parameter
describe_pet("Buddy")

Variable-Length Arguments

  1. *args: Capture positional arguments as a tuple.
  2. **kwargs: Capture keyword arguments as a dictionary.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def print_args(*args):
    """Print all positional arguments."""
    for arg in args:
        print(arg)

def print_kwargs(**kwargs):
    """Print all keyword arguments."""
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_args("apple", "banana", "cherry")
print_kwargs(name="Alice", age=25, city="New York")

Modules & Packages

What is a Module?

A module is a single Python file containing reusable code. It can include variables, functions, classes, or other Python objects, and it helps in organizing and reusing code across projects.

Creating a Module: Simply create a .py file with Python code.

1
2
3
4
5
6
# math_operations.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

Using a Module: You can use the import statement to include a module in another script.

1
2
3
4
import math_operations

print(math_operations.add(5, 3))        # Output: 8
print(math_operations.subtract(5, 3))   # Output: 2

Aliasing Modules:

1
2
import math_operations as mo
print(mo.add(5, 3))  # Output: 8

__name__ variable:

When Python scripts are executed, a special variable called __name__ is automatically set, Its value depends on whether the script is being run directly or imported as a module.

  • Direct Execution vs. Importing:

    • If the script is executed directly, __name__ is set to __main__.
    • If the script is imported, name is set to the name of the module.
  • Why Use if __name__ == "__main__":?

    This construct allows you to include code in a module that executes only when the file is run directly, not when it is imported as part of another program:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# math_operations.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

if __name__ == "__main__":
    print("Running as a standalone script")
    print(add(5, 3))
    print(subtract(10, 4))
  • How It Works:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Running the script directly
$ python math_operations.py
Running as a standalone script
8
6

# Importing the script as a module
>>> import math_operations
>>> math_operations.add(2, 3)
5

This ensures that the standalone script behavior and reusable module functionality are cleanly separated.

What is a Package?

A package is a collection of modules organized in directories. It contains an __init__.py file, which marks the directory as a Python package. Packages allow hierarchical structuring of modules, making them scalable for large projects.

Creating a Package:

  1. Create a directory structure:

    1
    2
    3
    
    my_package/
        __init__.py
        math_operations.py
    
  2. Add code to the modules: math_operations.py

    1
    2
    
    def multiply(a, b):
        return a * b
    
  3. Use the package in your project:

    1
    2
    3
    
    from my_package import math_operations
    
    print(math_operations.multiply(4, 5))   # Output: 20
    

Exception Handling

Exception handling allows developers to manage runtime errors gracefully, ensuring that a program does not crash unexpectedly and can provide meaningful feedback to users. Python achieves this through the try, except, else, and finally blocks.

What Are Exceptions?

An exception is an event that disrupts the normal flow of a program. Common examples of exceptions include:

  • ZeroDivisionError: Division by zero.
  • FileNotFoundError: File operations fail because the file does not exist.
  • ValueError: Invalid value passed to a function.
  • KeyError: Nonexistent key accessed in a dictionary.

Syntax:

1
2
3
4
5
6
7
8
try:
    # Code that might raise an exception
except SomeException:
    # Code to handle the exception
else:
    # Code to execute if no exception occurs
finally:
    # Code that always executes

Example 1: Handling a Specific Exception

1
2
3
4
try:
    result = 10 / 0
except ZeroDivisionError:
    print("You cannot divide by zero!")

Example 2: Catching Multiple Exceptions

You can handle different exceptions separately:

1
2
3
4
5
6
7
try:
    num = int(input("Enter a number: "))
    result = 10 / num
except ValueError:
    print("Invalid input! Please enter a number.")
except ZeroDivisionError:
    print("Division by zero is not allowed.")

Example 3: Using else

The else block executes if no exception occurs:

1
2
3
4
5
6
7
try:
    num = int(input("Enter a number: "))
    result = 10 / num
except ZeroDivisionError:
    print("Division by zero is not allowed.")
else:
    print("Result is:", result)

Example 4: Using finally

The finally block executes regardless of whether an exception occurs:

1
2
3
4
5
6
7
8
try:
    file = open('example.txt', 'r')
    content = file.read()
except FileNotFoundError:
    print("File not found!")
finally:
    file.close()
    print("File closed.")

Raising Exceptions:

You can raise exceptions explicitly using the raise keyword:

1
2
3
4
5
6
7
8
9
def check_age(age):
    if age < 18:
        raise ValueError("Age must be at least 18.")
    return "Age is valid."

try:
    print(check_age(16))
except ValueError as e:
    print("Error:", e)

Custom Exceptions:

You can define your own exceptions by creating a custom class derived from Exception:

1
2
3
4
5
6
7
class CustomError(Exception):
    pass

try:
    raise CustomError("This is a custom exception.")
except CustomError as e:
    print("Caught:", e)

File Handling

File handling in Python allows you to read from and write to files, enabling persistent data storage. Python provides built-in functions and methods for file operations, making it straightforward to work with files.

File Handling Basics:

To work with files, the open() function is used. Its syntax is:

1
open(file, mode)
  • file: The name or path of the file.
  • mode: Specifies the purpose of opening the file (e.g., read, write, append).

File Modes:

ModeDescription
rRead mode (default). File must exist.
wWrite mode. Creates a file or truncates it.
aAppend mode. Writes to the end of the file.
xExclusive creation. Fails if the file exists.
bBinary mode (e.g., rb, wb).
tText mode (default, e.g., rt, wt).

Reading Files:

Python provides various methods to read file content:

  1. Reading the Entire File:

    1
    2
    3
    
    with open('example.txt', 'r') as file:
        content = file.read()
        print(content)
    
  2. Reading Line by Line:

    1
    2
    3
    
    with open('example.txt', 'r') as file:
        for line in file:
            print(line.strip())
    
  3. Reading a Fixed Number of Characters:

    1
    2
    3
    
    with open('example.txt', 'r') as file:
        content = file.read(10)  # Reads the first 10 characters
        print(content)
    

Writing to Files

You can write to a file using the write() method.

1
2
with open('example.txt', 'w') as file:
    file.write("Hello, Python!\n")

To append content:

1
2
with open('example.txt', 'a') as file:
    file.write("Appending a new line.\n")

File Handling with with

Using the with statement ensures that the file is properly closed after operations, even if exceptions occur.

1
2
3
with open('example.txt', 'r') as file:
    content = file.read()
print(file.closed)  # Output: True

Checking if a File Exists Use the os module to check for file existence.

1
2
3
4
5
6
import os

if os.path.exists('example.txt'):
    print("File exists")
else:
    print("File not found")

Example

Here’s a complete example of reading from one file and writing to another:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
try:
    with open('input.txt', 'r') as infile:
        data = infile.read()

    with open('output.txt', 'w') as outfile:
        outfile.write(data.upper())

    print("Data processed successfully.")
except FileNotFoundError:
    print("File not found!")
except Exception as e:
    print(f"An error occurred: {e}")

Regular expressions

Regular Expressions (regex) are powerful tools for pattern matching and text manipulation, frequently used in DevOps for tasks like log analysis, input validation, and parsing configuration files. Python provides the re module to work with regular expressions efficiently.

Syntax:

  • Literals: Characters that match themselves, such as a, 1, @.

  • Metacharacters: Special characters with a specific meaning:

    • .: Matches any single character except a newline.
    • ^: Anchors the match to the beginning of the string.
    • $: Anchors the match to the end of the string.
    • []: Character class to match any one of the characters inside.
    • |: Alternation operator (OR), e.g., a|b matches either a or b.
    • (): Groups patterns together for capturing.
  • Quantifiers: Control how many times a pattern should occur:

    • *: 0 or more occurrences.
    • +: 1 or more occurrences.
    • ?: 0 or 1 occurrence.
    • {n,m}: Between n and m occurrences.
  • Escape Sequences:

    • \d: Matches any digit (0-9).
    • \w: Matches any word character (letters, digits, underscore).
    • \s: Matches any whitespace character (spaces, tabs, newlines).

To work with regular expressions in Python, you first need to import the re module.

1
import re

Basic Functions in re Module

  • re.match(): Checks if the regular expression matches the beginning of the string.

    1
    2
    3
    
    pattern = r"^Hello"
    result = re.match(pattern, "Hello World")
    print(result.group())  # Output: Hello
    
  • re.search(): Searches for the pattern anywhere in the string.

    1
    2
    3
    
    pattern = r"World"
    result = re.search(pattern, "Hello World")
    print(result.group())  # Output: World
    
  • re.findall(): Returns all non-overlapping matches in a string as a list.

    1
    2
    3
    
    pattern = r"\d+"  # Matches one or more digits
    result = re.findall(pattern, "There are 15 apples and 7 oranges.")
    print(result)  # Output: ['15', '7']
    
  • re.sub(): Replaces occurrences of the pattern with a specified replacement.

    1
    2
    3
    
    pattern = r"apples"
    result = re.sub(pattern, "bananas", "There are 15 apples and 7 oranges.")
    print(result)  # Output: There are 15 bananas and 7 oranges.
    
  • re.split(): Splits the string based on the given pattern.

    1
    2
    3
    
    pattern = r"\s+"  # Split on one or more whitespace characters
    result = re.split(pattern, "This is a sample sentence.")
    print(result)  # Output: ['This', 'is', 'a', 'sample', 'sentence.']
    

Lambdas

Lambda functions, also known as anonymous functions, are small, unnamed functions defined using the lambda keyword. They are commonly used when you need a simple function for a short period of time.

Syntax:

1
lambda arguments: expression
  • The arguments represent the inputs to the function.
  • The expression is a single statement that gets evaluated and returned.

Example:

1
2
square = lambda x: x ** 2
print(square(5))  # Output: 25

Lambda functions are often used in combination with higher-order functions like map(), filter(), and reduce().

  1. With map(): The map() function applies a function to all items in an iterable.

    1
    2
    3
    
    numbers = [1, 2, 3, 4]
    squared = map(lambda x: x ** 2, numbers)
    print(list(squared))  # Output: [1, 4, 9, 16]
    
  2. With filter(): The filter() function filters elements in an iterable based on a condition.

    1
    2
    3
    
    numbers = [1, 2, 3, 4, 5, 6]
    even_numbers = filter(lambda x: x % 2 == 0, numbers)
    print(list(even_numbers))  # Output: [2, 4, 6]
    
  3. With reduce(): The reduce() function, from the functools module, applies a rolling computation to a sequence.

    1
    2
    3
    4
    5
    
    from functools import reduce
    
    numbers = [1, 2, 3, 4]
    product = reduce(lambda x, y: x * y, numbers)
    print(product)  # Output: 24
    

Closures

A closure is a function object that retains access to variables in its enclosing lexical scope, even after the scope has finished executing. Closures are a powerful tool for creating functions with behavior that depends on the context in which they were defined.

For a closure to occur, three conditions must be met:

  1. A nested function must exist inside an enclosing function.
  2. The nested function must reference a variable defined in the enclosing scope.
  3. The enclosing function must return the nested function.

Example:

1
2
3
4
5
6
7
8
9
def outer_function(message):
    def inner_function():
        print(message)
    return inner_function

closure_function = outer_function("Hello from the closure!")
closure_function()  

# Output: Hello from the closure!=

Decorators

Decorators in Python are a powerful feature that allows you to modify the behavior of functions or methods. They are a key tool for writing concise, readable, and reusable code.

A decorator is essentially a function that takes another function as an argument and extends or alters its behavior. Decorators are often used for tasks such as logging, authentication, and performance monitoring.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

# Output
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

Generators

Generators are a type of iterable, like lists or tuples, that yield items one at a time using the yield keyword. Unlike lists, they don’t store all values in memory, making them ideal for large data sets or infinite sequences.

A generator is a function that produces a sequence of values. When a generator function is called, it doesn’t execute immediately. Instead, it returns a generator object that can be iterated over.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

for number in count_up_to(5):
    print(number)

# Output
# 1
# 2
# 3
# 4
# 5

Object oriented programming

Key concepts

  • A class acts as a blueprint or template for creating objects. It defines the attributes (data) and methods (behaviors) that its objects will possess.
  • Objects are instances of a class. Each object has its own set of attribute values, which can be unique.
  • Attributes represent the characteristics or properties of an object. They are defined as variables within a class and can hold data specific to each instance.
  • Methods are functions that belong to a class and define the actions or behaviors that objects can perform. They often operate on the object’s attributes.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Car:
    def __init__(self, brand, model, year): # Constructor (special method)
        self.brand = brand
        self.model = model
        self.year = year

    def display_info(self):
        return f"{self.year} {self.brand} {self.model}"

# Creating an object (instance of the class)
my_car = Car("Toyota", "Corolla", 2021)

# Access attributes
print(my_car.brand)  # Output: Toyota

# Call methods
print(my_car.display_info()) # Output:  2021 Toyota Corolla

Polymorphism

Polymorphism allows objects of different classes to be treated as instances of a common superclass. This means that the same method name can be used for different objects, with each object responding in its own specific way based on its class. It provides flexibility and enables writing more generic and reusable code.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

# Polymorphism in action
animals = [Dog(), Cat()]

for animal in animals:
    print(animal.speak())

# Output
# Woof!  
# Meow!  

Inheritance

Inheritance allows a class to reuse the properties and methods of parent class.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass

class Dog(Animal):
    def __init__(self,name, breed):
        super().__init__(name) # calls the parent class's constructor
        self.breed = breed
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Using the derived classes
dog = Dog("Buddy", "German Shepherd")
cat = Cat("Kitty")
print(dog.speak())  # Output: Buddy says Woof!
print(dog.breed)  # Output: German Shepherd!
print(cat.speak())  # Output: Kitty says Meow!

The super().__init__(name) call in the Dog class’s constructor invokes the constructor of the parent class to initialize the name attribute.

Encapsulation

Encapsulation is a fundamental principle of object-oriented programming that restricts direct access to certain components of an object. This prevents unintended interference and ensures the object’s internal state is only modified through well-defined methods.

In Python, encapsulation is achieved using underscores to indicate private or protected attributes:

  1. Single Underscore (_): Used as a convention to indicate that an attribute or method is intended for internal use. It doesn’t prevent access but signals that it shouldn’t be accessed directly by external code.
  2. Double Underscore (__): Enforces name mangling, making the attribute or method more difficult to access directly. This is used to implement stricter encapsulation.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Strongly private attribute

    def deposit(self, amount):
        if self.__is_valid_amount(amount):  # Using the private method
            self.__balance += amount
        else:
            print("Deposit amount must be positive")

    def withdraw(self, amount):
        if self.__is_valid_amount(amount):  # Using the private method
            if amount <= self.__balance:
                self.__balance -= amount
            else:
                print("Insufficient funds")
        else:
            print("Withdrawal amount must be positive")

    def get_balance(self):
        return self.__balance

    # Private method to validate amounts
    def __is_valid_amount(self, amount):
        return amount > 0

# Usage
account = BankAccount(1000)
account.deposit(500)        # Valid deposit
account.withdraw(200)       # Valid withdrawal
account.withdraw(-50)       # Invalid withdrawal amount
print(account.get_balance())  # Output: 1300

# Attempt to call the private method directly (will fail)
# print(account.__is_valid_amount(50))  # AttributeError

Abstraction

Abstraction focuses on hiding the implementation details of a class and exposing only the essential features. It simplifies the design of a system by reducing complexity and allowing developers to work with higher-level concepts without worrying about the underlying implementation. Python achieves abstraction through abstract base classes (ABCs)

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from abc import ABC, abstractmethod

# Abstract Base Class
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# Concrete Class
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# Usage
rect = Rectangle(10, 5)
print(rect.area())  # Output: 50

That’s all for this refresher! Feel free to reach out with any questions or feedback. Happy coding! 🚀