Skip to main content

Command Palette

Search for a command to run...

Python: A Comprehensive Tutorial

Last Updated: 12-08-2024

Updated
49 min read
Python: A Comprehensive Tutorial
J

I create cross platform mobile apps with AI functionalities. Currently a PhD Scholar at Indira Gandhi Delhi Technical University for Women, Delhi. M.Tech in Artificial Intelligence (AI).

Python is a high-level, interpreted, interactive and object-oriented scripting and general-purpose programming language.

Python was developed by Guido van Rossum, a Dutch programmer, currently a Distinguished Engineer at Microsoft.

Python supports multi-paradigm:

  1. Object-oriented: Define classes and create objects that encapsulate data and methods.

  2. Procedural (imperative): writing sequences of instructions and procedures to manipulate data and control program flow

  3. Functional: writing functions as first-class objects that can be passed around and combined to perform operations

  4. Structured: breaking down programs into functions and using control flow constructs to manage execution

  5. Reflective: to inspect and modify the program’s structure and behavior at runtime.

Pre-Requisite:

You will need a computer and python 3.7 on it.

File Extensions

Filename extensions: .py, .pyw, .pyz, .pyi, .pyc, .pyd

All these file extensions are used for different purposes.

  1. .py: This is the standard file extension for Python source code files. They contain plain text code written in Python and are executed using the Python interpreter.

  2. .pyw: This extension is used for Python scripts that are run in a Windows environment but should not open a command-line window when executed. It’s typically used for GUI applications that need to run without a terminal window.

  3. .pyz: This extension represents a Python zip archive. It contains a collection of Python files bundled together in a compressed format, often used for distributing Python applications. These archives are self-contained and can be executed as standalone programs if they include a main.py file.

  4. .pyi: These files are used for type hinting in Python. They are "stub" files that provide type information for Python code, useful for type checkers and IDEs to help with code analysis and autocompletion.

  5. .pyc: These are compiled Python files. When a .py file is executed, Python compiles it into bytecode, which is stored in a .pyc file. These files are used to make program execution faster, as Python can skip the compilation step if the .pyc file is up-to-date.

  6. .pyd: This is a dynamic library file used in Python, similar to a shared library or DLL in other languages. It contains compiled C or C++ code that can be imported and used in Python programs. These are often used to interface with C/C++ code from Python.

Python Basics

Identifiers

An identifier is a name given to entities like class, functions, variables, etc. It helps to differentiate one entity from another.

Rules for writing Identifiers

  1. Identifiers can be a combination of letters in lowercase (a to z) or uppercase (A to Z) or digits (0 to 9) or an underscore . Names like myClass, var1 and print_this_to_screen, all are valid example.

     # Valid Identifiers
     id = 24
    
  2. An identifier cannot start with a digit. 1variable is invalid, but variable1 is a valid name.

     # Invalid Identifiers
     1id = 24 # Gives Error
    
  3. Keywords cannot be used as identifiers.

     # Using keywords as identifier will give error
     def main():
         if = 10  # 'if' is a reserved keyword
         print(if)
     main()
    

    Output:

  4. We cannot use special symbols like !, @, #, $, % etc. in our identifier.

     def main():
         @value = 100  # '@' is not allowed in identifiers
         print(@value)
     main()
    

    Output:

  5. An identifier can be of any length.

     very_long_identifier_name_that_exceeds_normal_length_limits = "How you doing?"
     print(very_long_identifier_name_that_exceeds_normal_length_limits)
    

Tips

  • Python is a case-sensitive language.

  • Always give the identifiers a name that makes sense.

  • Multiple words can be separated using an underscore, like this_is_a_long_variable.

Statement, Indentation and Comments

Statement

A statement is an instruction that the Python interpreter can execute

Single Line Statement

# Assignment Statements: Assign values to variables.
x = 10

# Expression Statement: Evaluate an expression and then execute it.
y = x + 3

Multi-Line Statement

We can use a backslash (\) to indicate that a statement continues on the next line.

y = 1 + 2 + \
    3 + 4 + \
    5 + 6

Python implicitly continues a statement across multiple lines when enclosed in parentheses (), brackets [], or braces {}. This is often preferred for better readability.

result = (
    1 + 2 + 3 + 4 +
    5 + 6 + 7 + 8 +
    9 + 10
)

For multi-line strings, we can use triple quotes (''' or """). This is useful for long strings or docstrings.

long_string = """This is a long string
that spans multiple lines.
We can include line breaks and other formatting here."""

Multiple Statements in single line

We can also put multiple statements in a single line using semicolons,

a=1; b=2;c=3;

Indentation

Python uses indentation to define a block of code.

A code block (body of a function, loop, etc.) starts with indentation and ends with the first unindented line. The amount of indentation is up to us, but it must be consistent throughout that block.

# Example of indentation in Python

# Function definition with indentation
def greet(name):
    # Indented block for the function body
    if name:
        # Further indentation for the if block
        print(f"Hello, {name}!")
    else:
        # Further indentation for the else block
        print("Hello, stranger!")

# Calling the function
greet("Alice")

Comments

In Python, comments are used to explain code, making it easier to understand for other programmers. They are ignored by the Python interpreter and do not affect the execution of the code.

Single Line Comments

Single-line comments start with the hash mark (#). Everything after the # on that line is considered a comment and is ignored by the interpreter.

# This is a single-line comment
x = 10  # This is an inline comment

Multi-line Comments

Python does not have a specific syntax for multi-line comments. Instead, multi-line comments are typically written using a series of single-line comments or a string literal. Though string literals (triple quotes) are often used for multi-line comments, they are technically string literals and not true comments.

# This is a multi-line comment
# It spans several lines
# and is created using consecutive single-line comments

"""
This is a multi-line comment
using triple quotes. This is actually a string literal,
but it can be used to comment out code or add documentation.
"""

Docstrings are used to document modules, classes, and functions. They are written using triple quotes and should be placed right after the definition of the module, class, or function.

def add(a, b):
    """ Function to add two numbers and return the result """
    return a + b

Variables

A variable is a named location used to store data in the memory. It is helpful to think of variables as a container that holds data that can be changed later in the program.

num = 15

Here, we have created a variable named 'num'. We have assigned the value 15 to the variable. We can think of variables as a bag to store books in it and that book can be replaced at any time. The assignment operator = is used to assign a value to a variable.

Example 1: Declaring and assigning value to variables

website = "www.google.com"
print(website)

Example 2: Changing the value of a variable

website = "www.google.com"
print(website)

# assinging a new value to website
website = "www.bing.com"
print(website)

Example 3: Assigning multiple values to multiple variables

a, b, c = 10, 3.2, "Hello"
x = y = z = "same value"
print(a)
print(b)
print(c)
print(x,y,z)

Example 4: Declaring and assigning value to constant

Python does not have built-in support for constants, so it's a convention to use uppercase variable names to indicate that a value is intended to be constant.

PI = 3.14
GRAVITY = 9.8

Data Types

Data types represent the kind of value a variable can hold. Python supports a variety of data types, each serving different purposes.

There are two types of Data Types

  1. Built-in Data Types: These are pre-built-in python by default.

  2. User-Defined Data Types: These are defined by users.

Built-in Data Types

Numeric Types

  1. int: Integer type

  2. float: Floating-point numbers (decimals).

  3. complex: Complex numbers with a real and imaginary part.

# integer type
a = 10

# Float type
b = -0.001

# complex type
c = 2 + 3j

Sequence Types

  1. str: Strings, which are sequences of characters.

  2. list: Ordered, mutable sequences of items.

  3. tuple: Ordered, immutable sequences of items.

s = "Hello, world!" # String
numbers = [1, 2, 3, 4, 5] # List
point = (10, 20, 30) # Tuple

Mapping Types

  1. dict: Dictionaries, which are collections of key-value pairs
# dictionary
person = {"name": "Alice", "age": 30, "city": "New York"}

Set Types

  1. set: Unordered collections of unique items

  2. frozenset: Immutable sets.

unique_numbers_set = {1, 2, 3, 4, 5} # set
immutable_set = frozenset([1, 2, 3, 4, 5]) # Frozen set

Boolean Types

  1. bool: Boolean values, which can be True or False
is_active = True
is_empty = False

Binary Types

  1. byte: Immutable sequences of bytes

  2. bytearray: Mutable sequences of bytes

  3. memoryview: A view object that exposes an array’s data buffer.

b = b"Hello" # byte
ba = bytearray(b"Hello") # bytearray
mv = memoryview(b"Hello") # memoryview

User-Defined Data Types

User-defined data types in Python are custom types that we create to model specific kinds of data in our programs. They are typically created using classes, allowing us to define the structure and behavior of these types.

  1. Classes: Define complex data types with attributes and methods.

  2. Named Tuples: Provide immutable data structures with named fields.

  3. Data Classes: Simplify the creation of classes for storing data with automatic methods.

  4. Enumerations: Define a set of named constants.

  5. Custom Iterators: Implement custom iteration logic using __iter__() and __next__().

  6. Custom Context Managers: Manage resources using __enter__() and __exit__() with the with statement.

Classes

A class is a blueprint for creating objects. Classes can have attributes (data) and methods (functions) to define their behavior.

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

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

# Create an instance of the Car class
my_car = Car("Toyota", "Corolla", 2021)
print(my_car.display_info())  # Output: 2021 Toyota Corolla

Named Tuples

Named tuples are a type of tuple with named fields, which can be accessed like attributes. They are useful for simple data structures that don’t need methods.

from collections import namedtuple

Person = namedtuple('Person', ['name', 'age'])

# Create an instance of the Person named tuple
p = Person(name="Alice", age=30)
print(p.name)  # Output: Alice
print(p.age)   # Output: 30

Data Classes

Data classes, introduced in Python 3.7, simplify the creation of classes for storing data by automatically generating special methods such as __init__, __repr__, and __eq__.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

# Create an instance of the Person data class
p = Person(name="Alice", age=30)
print(p)  # Output: Person(name='Alice', age=30)

Custom Enumerations

Enumerations (enums) are a way to define a set of named values. They are used to create a collection of constants.

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# Access enum members
print(Color.RED)        # Output: Color.RED
print(Color.RED.name)   # Output: RED
print(Color.RED.value)  # Output: 1

Custom Iterators

Custom iterators are objects that implement the iterator protocol, consisting of __iter__() and __next__() methods.

class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

# Use the custom iterator
for num in Counter(3, 8):
    print(num)  # Output: 3 4 5 6 7 8

Custom Context Manager

Custom context managers are used to manage resources efficiently and are implemented using the __enter__() and __exit__() methods. They are typically used with the 'with' statement.

class FileManager:
    def __init__(self, filename):
        self.filename = filename

    def __enter__(self):
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        self.file.close()

# Use the custom context manager
with FileManager('example.txt') as file:
    file.write('Hello, world!')

These user-defined data types enable one to model and manage complex data in a way that is tailored to the application's needs.

Keywords

Keywords are reserved words in python. Keywords must be used for the purpose it is made for.

  • Keyword cannot be used as a variable name, function name or any other identifier. They are used to define the syntax and structure of the Python language.

  • All keywords are case sensitive.

  • There are 33 keywords in Python 3.7.

  • All the keywords except True, False and None are in lowercase and they must be written as they are.

Let's see each keyword one by one and their use case.

True: Boolean value representing true.

flag = True

False: Boolean value representing false

flag = False

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

result = None

and: Logical operator that returns True if both operands are true.

if a > 0 and b > 0:
    print("Both are positive")

as: Used to create an alias while importing modules or handling exceptions.

import numpy as np

in: Checks if a value exists within an iterable or performs membership tests.

if x in [1, 2, 3]:
    print("x is in the list")

is: Tests for object identity, whether two variables point to the same object.

if a is b:
    print("a and b are the same object")

not: Logical operator that negates a boolean expression.

if not x:
    print("x is False or None")

or: Logical operator that returns True if at least one of the operands is true.

if a > 0 or b > 0:
    print("At least one is positive")

from: Used to import specific parts of a module.

from math import sqrt

with: Used to wrap the execution of a block of code within methods defined by the context manager. It ensures that resources are properly managed.

with open("file.txt", "r") as file:
    contents = file.read()

import: Used to import modules into the current namespace.

import math

global: Declares a global variable inside a function.

def my_function():
    global x
    x = 10

nonlocal: Refers to a variable in the nearest enclosing scope that is not global.

def outer_function():
    x = 10
    def inner_function():
        nonlocal x
        x = 20
    inner_function()
    print(x)

class: Used to define a new class

class MyClass:
    pass

assert: Used for debugging purposes to test if a condition is true. Raises an AssertionError if the condition is false.

assert x > 0, "x must be positive"

async: used to define a coroutine. A coroutine is a special type of function that can pause and resume its execution. Coroutines are defined using the "async def" syntax.

async def fetch_data():
    print("Fetching data...")
    await asyncio.sleep(2)  # Simulate a network delay
    print("Data fetched")
    return "some data"

await: used inside an async function to pause its execution until a coroutine or an awaitable object completes. await can only be used within async functions. It effectively tells the Python interpreter to wait for the result of the coroutine before continuing

import asyncio

async def main():
    result = await fetch_data()  # Awaiting the coroutine
    print(result)

asyncio.run(main())
  • Use async def to define a coroutine. This function will return a coroutine object, which can be awaited.

  • Inside an async function, use await to pause execution until the awaited coroutine completes. This allows other tasks to run concurrently.

  • Use asyncio.run() to execute the top-level coroutine. This function runs the event loop, executes the coroutine, and closes the loop when done.

if: Used to start a conditional statement.

if x > 0:
    print("Positive")

else: Used in conditional statements and loops. Executes a block of code if the condition in if or while is false, or when a loop terminates normally.

if x > 0:
    print("Positive")
else:
    print("Non-positive")

elif: Used in conditional statements as an abbreviation for "else if"

if x > 0:
    print("Positive")
elif x < 0:
    print("Negative")
else:
    print("Zero")

break: Exits the closest enclosing loop.

for i in range(10):
    if i == 5:
        break

continue: Skips the rest of the code inside the current loop iteration and proceeds to the next iteration.

for i in range(10):
    if i % 2 == 0:
        continue
    print(i)

pass: A null statement used as a placeholder. It does nothing and is used when a statement is required syntactically but we don’t want to execute any code.

def function_that_does_nothing():
    pass

def: Used to define a function.

def my_function(param1):
    return param1 * 2

lambda: Creates an anonymous function (a function without a name).

square = lambda x: x * x

del: Deletes an object or a reference to an object.

del my_list

for: Used to create a loop that iterates over a sequence (like a list, tuple, or range).

for i in range(5):
    print(i)

while: Creates a loop that continues as long as a condition is true.

while x > 0:
    print(x)
    x -= 1

raise: Raises an exception

raise ValueError("A value error occurred")

return: Exits a function and optionally returns a value.

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

yield: Used in a generator function to yield a value and pause the function’s execution, allowing it to resume later.

def count_up_to(max):
    count = 1
    while count <= max:
        yield count
        count += 1

try: Used to start a block of code that will be tested for exceptions.

except: Used to catch and handle exceptions raised in a try block.

finally: Used to define a block of code that will be executed no matter what, regardless of whether an exception was raised or not.

try:
    file = open('example.txt', 'r')
    # Perform file operations
except FileNotFoundError:
    print("File not found")
finally:
    file.close()
    print("File closed")

Type Conversion and Type Casting

  • Type conversion refers to the process of converting a value from one type to another type. This can be done implicitly or explicitly.

  • Implicit Conversion: Python automatically converts between types when it's safe and logical to do so. For example, when performing arithmetic operations with integers and floats, Python will implicitly convert integers to floats to ensure accuracy

  • Explicit Conversion: Also known as type casting, explicit conversion is when we manually convert between types using functions like int(), float(), str(), etc.

Type Conversion Examples

Python provides functions to convert between different data types

To int

# From float to int
num_float = 12.34
num_int = int(num_float)  # 12

# From string to int
num_str = "123"
num_int = int(num_str)  # 123

# Note: Strings should represent integers to convert successfully.
# num_str = "12.34" would raise a ValueError

To Float

# From int to float
num_int = 12
num_float = float(num_int)  # 12.0

# From string to float
num_str = "12.34"
num_float = float(num_str)  # 12.34

To str

# From int to string
num_int = 123
num_str = str(num_int)  # "123"

# From float to string
num_float = 12.34
num_str = str(num_float)  # "12.34"

To list

# From string to list of characters
str_data = "hello"
list_data = list(str_data)  # ['h', 'e', 'l', 'l', 'o']

# From tuple to list
tuple_data = (1, 2, 3)
list_data = list(tuple_data)  # [1, 2, 3]

To tuple

# From list to tuple
list_data = [1, 2, 3]
tuple_data = tuple(list_data)  # (1, 2, 3)

# From string to tuple of characters
str_data = "hello"
tuple_data = tuple(str_data)  # ('h', 'e', 'l', 'l', 'o')

To set

# From list to set
list_data = [1, 2, 2, 3]
set_data = set(list_data)  # {1, 2, 3}

# From string to set of characters
str_data = "hello"
set_data = set(str_data)  # {'h', 'e', 'l', 'o'}

To dict

# From list of tuples to dict
list_data = [("a", 1), ("b", 2), ("c", 3)]
dict_data = dict(list_data)  # {'a': 1, 'b': 2, 'c': 3}

# From list of lists to dict
list_data = [["a", 1], ["b", 2], ["c", 3]]
dict_data = dict(list_data)  # {'a': 1, 'b': 2, 'c': 3}

Python I/O and File Handling

In Python, input and output operations are fundamental for interacting with users and handling data.

Input

To get input from the user, we use the input() function. This function reads a line from input (usually from the keyboard) and returns it as a string.

user_input = input("Enter something: ")
print(f"You entered: {user_input}")

Output

To display output, we use the print() function. It sends the specified text or data to the standard output (usually the console).

print("Hello, World!")

File Modes

File modes determine how we can interact with a file when we open it. These modes specify whether we want to read, write, or append to a file and if we want to handle it as text or binary data.

  • 'r' – Read mode (default).

  • 'w' – Write mode (creates a new file or truncates an existing file).

  • 'a' – Append mode (writes to the end of the file).

  • 'b' – Binary mode (for binary files).

Reading and Writing Files

We can read from and write to files using the open() function along with read(), write(), and other methods.

# Reading from a File
with open('example.txt', 'r') as file:
    content = file.read()
    print(content)

# Writing to a file
with open('example2.txt', 'w') as file2:
    file2.write("Hello, file!")

Handling Files with Context Managers

Using with (context managers) ensures that files are properly closed after their block of code is executed, even if an error occurs.

with open('example.txt', 'r') as file:
    data = file.read()
# File is automatically closed here

Reading File Line by Line

with open('example.txt', 'r') as file:
    for line in file:
        print(line.strip())

Reading and Writing Binary Files

To handle binary files, use the 'b' mode in open(). This is useful for files such as images or executable files.

Reading Binary Files

with open('example.jpg', 'rb') as file:
    data = file.read()

Writing Binary Files

with open('output.bin', 'wb') as file:
    file.write(data)

Reading and Writing CSV files

Python's csv module is handy for handling CSV files.

Reading CSV files

import csv

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

Writing CSV files

import csv

data = [
    ["Name", "Age"],
    ["Alice", 30],
    ["Bob", 25]
]

with open('example.csv', 'w', newline='') as file:
    writer = csv.writer(file)
    writer.writerows(data)

Reading and Writing JSON files

The json module helps in working with JSON data.

Reading JSON files

import json

with open('example.json', 'r') as file:
    data = json.load(file)
    print(data)

Writing JSON files

import json

data = {"name": "Alice", "age": 30}

with open('example.json', 'w') as file:
    json.dump(data, file)

Reading and Writing Pickle Module

The pickle module is used for serializing and deserializing Python objects.

Pickling (writing) an object

import pickle

data = {'key': 'value', 'number': 42}

with open('example.pkl', 'wb') as file:
    pickle.dump(data, file)

Unpickling (reading) an object

import pickle

with open('example.pkl', 'rb') as file:
    data = pickle.load(file)
    print(data)

Using io Module for In-Memory Streams

The io module provides tools for handling in-memory streams.

StringIO (in-memory text stream)

from io import StringIO
# Create an in-memory text stream
sio = StringIO("Initial data")
print(sio.read())  # Read the content
sio.write(" More data")  # Write to the stream
sio.seek(0)  # Rewind to the beginning
print(sio.read())  # Read again

BytesIO (in-memory binary stream)

from io import BytesIO

# Create an in-memory binary stream
bio = BytesIO(b"Initial binary data")
print(bio.read())  # Read the content
bio.write(b" More binary data")  # Write to the stream
bio.seek(0)  # Rewind to the beginning
print(bio.read())  # Read again

Files Path

Handling file paths in Python involves working with pathnames to ensure our code can correctly locate and manipulate files and directories. Python provides several modules to manage file paths effectively: os.path, and pathlib.

Using os.path

import os

# Join paths
path = os.path.join('folder', 'subfolder', 'file.txt')

# Check if a path exists
print(os.path.exists(path))

# Get the absolute path
print(os.path.abspath(path))

# Get the directory name
print(os.path.dirname(path))

# Get the base name (file name)
print(os.path.basename(path))

# Split the path into directory and file name
print(os.path.split(path))

# Check if it's a file
print(os.path.isfile(path))

# Check if it's a directory
print(os.path.isdir(path))

Using pathlib

The pathlib module, introduced in Python 3.4, provides an object-oriented interface for handling file system paths.

from pathlib import Path

# Create a Path object
path = Path('folder') / 'subfolder' / 'file.txt'

# Check if a path exists
print(path.exists())

# Get the absolute path
print(path.resolve())

# Get the directory name
print(path.parent)

# Get the file name
print(path.name)

# Split the path into directory and file name
print(path.parts)

# Check if it's a file
print(path.is_file())

# Check if it's a directory
print(path.is_dir())

Operators

Operators in Python are special symbols or keywords used to perform operations on values and variables. They can be categorized into several types based on their functionality.

  • Arithmetic Operators: Perform basic mathematical operations.

  • Comparison Operators: Compare two values.

  • Logical Operators: Perform logical operations.

  • Assignment Operators: Assign and update variable values.

  • Bitwise Operators: Perform bit-level operations.

  • Membership Operators: Check membership in sequences.

  • Identity Operators: Compare object identities

Arithmetic Operators

  • Addition (+): Adds two operands.

  • Subtraction (-): Subtracts the second operand from the first.

  • Multiplication (*): Multiplies two operands.

  • Division (/): Divides the first operand by the second and returns a float.

  • Floor Division (//): Divides the first operand by the second and returns the largest integer less than or equal to the result.

  • Modulus (%): Returns the remainder of the division

  • Exponentiation (**): Raises the first operand to the power of the second.

print(5 + 3)  # 8
print(5 - 3)  # 2
print(5 * 3)  # 15
print(5 / 2)  # 2.5
print(5 // 2)  # 2
print(5 % 2)  # 1
print(5 ** 2)  # 25

In the above code:

  • print(5 + 3) adds 5 and 3, resulting in 8.

  • print(5 - 3) subtracts 3 from 5, giving 2.

  • print(5 \ 3) multiplies 5 by 3, resulting in 15.*

  • print(5 / 2) divides 5 by 2, giving 2.5 (a float).

  • print(5 // 2) performs integer division, which divides 5 by 2 and drops the decimal part, resulting in 2.

  • print(5 % 2) finds the remainder when 5 is divided by 2, which is 1.

  • print(5 ** 2) raises 5 to the power of 2, resulting in 25.

Comparison Operators

Comparison operator is used to compare two values and gives Boolean value in return as True or False.

  • Equal to (==): Checks if two operands are equal.

  • Not equal to (!=): Checks if two operands are not equal.

  • Greater than (>): Checks if the first operand is greater than the second.

  • Less than (<): Checks if the first operand is less than the second.

  • Greater than or equal to (>=): Checks if the first operand is greater than or equal to the second.

  • Less than or equal to (<=): Checks if the first operand is less than or equal to the second.

print(5 == 3)  # False
print(5 != 3)   # True
print(5 > 3)   # True
print(5 < 3)   # False
print(5 >= 3)   # True
print(5 <= 3)   # False

In the above code

  • print(5 == 3) checks if 5 is equal to 3. Since they are not equal, the result is False.

  • print(5 != 3) checks if 5 is not equal to 3. Since they are not equal, the result is True.

  • print(5 > 3) checks if 5 is greater than 3. Since 5 is indeed greater, the result is True.

  • print(5 < 3) checks if 5 is less than 3. Since 5 is greater, the result is False.

  • print(5 >= 3) checks if 5 is greater than or equal to 3. Since 5 is greater, the result is True.

  • print(5 <= 3) checks if 5 is less than or equal to 3. Since 5 is greater, the result is False.

Logical Operators

Used to perform logical operations and gives True or False in return

  • And (and): Returns True if both operands are true.

  • Or (or): Returns True if at least one operand is true.

  • Not (not): Returns True if the operand is false.

print(True and False)  # False
print(True or False)  # True
print(not True)  # False

In the above code

  • print(True and False) uses the and operator, which returns True only if both values are True. Since one value is False, the result is False.

  • print(True or False) uses the or operator, which returns True if at least one value is True. Here, one value is True, so the result is True.

  • print(not True) uses the not operator to invert the value. Since not True is False, the result is False.

Assignment Operators

  • Assignment (=): Assigns the value on the right to the variable on the left.

  • Add and assign (+=): Adds and assigns.

  • Subtract and assign (-=): Subtracts and assigns.

  • Multiply and assign (*=): Multiplies and assigns.

  • Divide and assign (/=): Divides and assigns.

  • Floor divide and assign (//=): Floor divides and assigns.

  • Modulus and assign (%=): Modulus and assigns.

  • Exponentiate and assign (**=): Exponentiates and assigns.

a = 5
a += 3  # Equivalent to a = a + 3
a -= 3  # Equivalent to a = a - 3
a *= 3  # Equivalent to a = a * 3
a /= 3  # Equivalent to a = a / 3
a //= 3  # Equivalent to a = a // 3
a %= 3  # Equivalent to a = a % 3
a **= 3  # Equivalent to a = a ** 3

In the above code, a starts with the value 5. The code then uses shorthand operators to update a:

  • a += 3 adds 3 to a, so a becomes 8.

  • a -= 3 subtracts 3 from a, returning it to 5.

  • a \= 3 multiplies a by 3, changing it to 15.

  • a /= 3 divides a by 3, resulting in 5.0 (a float).

  • a //= 3 performs integer division by 3, changing a to 1.0 (still a float).

  • a %= 3 calculates the remainder of a divided by 3, making a equal to 1.0.

  • a *= 3 raises a to the power of 3, resulting in 1.0 (since 1.0 ** 3 is 1.0).

Bitwise Operators

  • And (&): Performs a bitwise AND operation.

  • Or (|): Performs a bitwise OR operation.

  • Xor (^): Performs a bitwise XOR operation.

  • Complement (~): Performs a bitwise NOT operation.

  • Left Shift (<<): Shifts bits to the left.

  • Right Shift (>>): Shifts bits to the right.

print(5 & 3)  # 1
print(5 | 3) # 7
print(5 ^ 3)  # 6
print(~5)  # -6
print(5 << 1)  # 10
print(5 >> 1)  # 2

In the above code, bitwise operations are done:

  • print(5 & 3) performs a bitwise AND between 5 (which is 101 in binary) and 3 (which is 011 in binary), resulting in 001 in binary, which is 1.

  • print(5 | 3) performs a bitwise OR, combining 101 and 011 to get 111 in binary, which is 7.

  • print(5 ^ 3) performs a bitwise XOR, where 101 XOR 011 results in 110 in binary, which is 6.

  • print(~5) performs a bitwise NOT, which inverts 101 to ...11111010 in binary (the two's complement representation), resulting in -6.

  • print(5 << 1) performs a left shift, moving 101 one position to the left to get 1010 in binary, which is 10.

  • print(5 >> 1) performs a right shift, moving 101 one position to the right to get 10 in binary, which is 2.

Membership Operators

  • In (in): Returns True if a value is found in a sequence.

  • Not in (not in): Returns True if a value is not found in a sequence.

print(5 in [1, 2, 3, 4, 5])  # True
print(6 not in [1, 2, 3, 4, 5])  # True

In the above code, print(5 in [1, 2, 3, 4, 5]) checks if the number 5 is present in the list [1, 2, 3, 4, 5]. Since 5 is indeed in the list, it prints True. On the other hand, print(6 not in [1, 2, 3, 4, 5]) checks if the number 6 is not in the list. Since 6 is not in the list, it prints True.

Identity Operators

  • Is (is): Returns True if both variables point to the same object.

  • Is not (is not): Returns True if both variables point to different objects.

a = [1, 2, 3]
b = a
print(a is b)  # True
print(a is not b) # False

In the above code, a is a list containing the elements [1, 2, 3]. The variable b is then set to refer to the same list as a. When we check a is b, it returns True because both a and b point to the exact same list in memory. Therefore, they are the same object. Conversely, a is not b returns False because a and b are indeed the same object, so it is not true that they are different.

If else and elif

If, elif, and else are used to make decisions and control the flow of our program based on conditions.

  • If Statement: The if statement evaluates a condition and executes the block of code that follows if the condition is True.

  • Elif statement: The elif (short for "else if") statement allows us to check multiple conditions. It follows an if statement and runs only if the preceding if (or elif) condition was False.

  • Else statement: The else statement runs a block of code if none of the preceding if or elif conditions are True. It’s optional and should be the final statement in the conditional chain.

score = 85

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

This code assigns a score of 85 to a variable and then uses a series of conditional statements to determine and print the corresponding grade. It first checks if the score is 90 or above, then 80 or above, and so on. Since 85 is greater than 80 but less than 90, it prints "Grade: B" and stops checking further conditions. If the score didn’t meet any of the higher thresholds, the code would continue to check lower ranges or default to "Grade: F" if none of the conditions were met.

Loops

Loops are used to execute a block of code repeatedly based on a condition or a sequence.

For Loop

The for loop iterates over a sequence (like a list, tuple, or string) or other iterable objects.

SYNTAX:

for variable in sequence:

# Code to execute on each iteration

for i in range(5):
    print(i)

This loop will print the numbers 0 through 4. The range(5) generates a sequence of numbers from 0 to 4.

For Loop with List

fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
    print(fruit)

This will print each fruit in the list: apple, banana, cherry.

For Loop with Tuple

fruits = ('apple', 'banana', 'cherry')
for fruit in fruits:
    print(fruit)

This will print each fruit in the tuple (which is an immutable data structure): apple, banana, cherry.

For Loop with String

word = 'hello'
for letter in word:
    print(letter)

This will print each letter in the string: h, e, l, l, o.

For loop with Dictionary

When we work with dictionary, we can iterate over keys, values and key-value pairs.

Iterating Over Keys to print each key (Alice, Bob, Charlie).

student_grades = {'Alice': 90, 'Bob': 85, 'Charlie': 92}
for student in student_grades:
    print(student)

Iterating Over values to print each value (90, 85, 92).

for grade in student_grades.values():
    print(grade)

Iterating Over key-value pairs to print each key-value pair (Alice: 90, Bob: 85, Charlie: 92).

for student, grade in student_grades.items():
    print(f"{student}: {grade}")

For Loop with Range

Iterate over a sequence of numbers generated by range().

In range function first argument is start value, second argument is stop value and third argument is step size. The function generates value from start_value till stop_value-1.

for i in range(0, 10, 2):
    print(i)

range(0, 10, 2) generates numbers starting from 0 up to, but not including, 10, with a step of 2. The above code will print value 0, 2, 4, 6, 8.

For else Loop

We can use an else block with a for loop. The else block runs after the loop has completed all its iterations, but only if the loop was not terminated by a break statement.

for number in range(3):
    print(number)
else:
    print("Loop completed")

This loop iterates over the numbers 0, 1, and 2, printing each number. After completing the loop, it prints "Loop completed", since the loop ended normally without a break.

for number in range(3):
    if number == 1:
        break
    print(number)
else:
    print("Loop completed")

The loop prints 0, but when number is 1, the break statement is triggered. The else block is skipped because the loop did not finish all iterations normally (it was terminated by break).

While Loop

The while loop repeatedly executes a block of code as long as a given condition remains True. The while loop is flexible and useful when the number of iterations is not known beforehand.

count = 0
while count < 5:
    print(count)
    count += 1

The loop starts with count set to 0. As long as count is less than 5, it prints the current value of count. It then increments count by 1 on each iteration. When count reaches 5, the condition count < 5 becomes False, and the loop stops.

While else Loop

A while loop can be paired with an else block. The else block runs after the loop has completed all its iterations, but only if the loop was not terminated by a break statement.

count = 0
while count < 3:
    print(count)
    count += 1
else:
    print("Loop finished")

The loop starts with count at 0 and runs while count is less than 3. It prints the current value of count and increments it. When count reaches 3, the condition count < 3 becomes False, and the loop terminates. The else block then executes, printing "Loop finished"

count = 0
while count < 5:
    if count == 2:
        break
    print(count)
    count += 1
else:
    print("Loop finished")

The loop prints 0 and 1. When count is 2, the break statement is triggered, exiting the loop. The else block is skipped because the loop did not complete all iterations normally (it was exited by break).

break and continue statements

The break and continue statements are used to control the flow of loops by modifying their behavior under certain conditions.

Break statement

The break statement immediately terminates the loop, regardless of the loop’s condition. Execution continues with the code following the loop. It is used to exit a loop early when a certain condition is met.

for i in range(5):
    if i == 3:
        break
    print(i)

This loop prints 0, 1, and 2. When i equals 3, the break statement is executed, and the loop exits. The number 3 is not printed, and the loop stops.

Continue statements

The continue statement skips the rest of the code inside the current iteration of the loop and proceeds to the next iteration. It does not terminate the loop.

It is used to commonly used to avoid executing certain code under specific conditions.

for i in range(5):
    if i == 3:
        continue
    print(i)

This loop prints 0, 1, 2, and 4. When i equals 3, the continue statement skips the print(i) statement for that iteration, so 3 is not printed. The loop then continues with the next iteration.

Functions

Functions are reusable blocks of code designed to perform a specific task. They help organize code, avoid repetition, and make programs more modular and readable.

Function Definition

We can define a function using the keyword 'def', followed by the function name, parentheses, and a colon. The function body is indented.

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

Explanation:

  • def starts the function definition.

  • greet is the name of the function.

  • name is a parameter (input) for the function.

  • The print statement inside the function prints a greeting message.

Function Call

To use a function, we can call it by its name and pass the required arguments (if any).

greet("Alice")  # Output: Hello, Alice!

This calls the greet function with "Alice" as the argument. The function executes and prints "Hello, Alice!".

Return Value

Functions can return a value using the return statement. If no return statement is used, the function returns None by default.

def add(a, b):
    return a + b
result = add(5, 3)
print(result)  # Output: 8

The add function takes two parameters, a and b, and returns their sum. result stores the return value of add(5, 3), which is 8.

Parameters and Arguments

  • Parameters: Variables listed in the function definition.

  • Arguments: Values passed to the function when calling it.

def describe_person(name, age):
    print(f"{name} is {age} years old.")

describe_person(age=25, name="Bob")

'age' and 'name' are passed as keyword arguments. We can change the order of arguments if while calling we use named arguments.

Pass by object reference

The concepts of "pass by value" and "pass by reference" describe how arguments are passed to functions. However, Python’s approach is a bit different from traditional pass-by-value or pass-by-reference models.

Pass by Value

In tradition programming languages, in pass-by-value, a copy of the argument's value is passed to the function. Changes made to the parameter inside the function do not affect the original argument.

Python does not use strict pass-by-value, but rather uses a model often described as "pass-by-object-reference" or "pass-by-assignment."

Pass by Reference

In traditional programming languages, in pass-by-reference, a reference (or pointer) to the argument is passed to the function. This means changes to the parameter will affect the original argument.

In python, it passes references to objects, but it does not provide direct access to the memory addresses like traditional pass-by-reference. Instead, it follows a model called "pass-by-object-reference" or "pass-by-assignment."

Pass by Object Reference

Immutable Objects: For immutable objects (e.g., integers, strings, tuples), we cannot change the object itself. We can reassign the parameter to a new value, but this does not affect the original object outside the function.

def modify_immutable(x):
    x = 10  # Reassigns x to a new integer object
    print(x)  # Output: 10

a = 5
modify_immutable(a)
print(a)  # Output: 5

In the above code, a remains 5 because integers are immutable, and reassigning x inside the function does not affect a.

Mutable Objects: For mutable objects (e.g., lists, dictionaries), we can modify the object itself. Changes to the object inside the function are reflected outside the function.

def modify_mutable(lst):
    lst.append(4)  # Modifies the list in place
    print(lst)  # Output: [1, 2, 3, 4]

my_list = [1, 2, 3]
modify_mutable(my_list)
print(my_list)  # Output: [1, 2, 3, 4]

In the above code, my_list is modified inside the function because lists are mutable, and changes to lst are reflected in my_list.

Types of Functions

Python has two type of functions:

  1. Built-in Functions: They are provided by Python and are available for use without needing to define them. They are part of Python’s standard library and offer a wide range of functionality for common tasks.

  2. User defined Functions: They are created by the programmer to perform specific tasks that are not covered by built-in functions. We define them using the def keyword.

Built-in functions

abs()

Returns the absolute value of a number

print(abs(-5))  # Output: 5

all()

Returns True if all elements in an iterable are true (or if the iterable is empty).

print(all([True, True, False]))  # Output: False
print(all([1, 2, 3]))            # Output: True

ascii ()

Returns a string containing a printable representation of an object, but escapes non-ASCII characters.

print(ascii("hello"))  # Output: 'hello'
print(ascii("helloñ")) # Output: 'hello\xf1'

bool()

Converts a value to a Boolean (True or False).

print(bool(0))      # Output: False
print(bool("text")) # Output: True

enumerate()

Adds a counter to an iterable and returns it as an enumerate object.

for index, value in enumerate(["a", "b", "c"]):
    print(index, value)
# Output: 
# 0 a
# 1 b
# 2 c

format()

Formats a value into a string according to a specified format.

print(format(123.456, ".2f"))  # Output: 123.46
print(format("text", "^10"))   # Output: '   text   '

getattr()

Retrieves an attribute from an object, with an optional default value if the attribute does not exist.

class MyClass:
    attr = "value"

obj = MyClass()
print(getattr(obj, 'attr'))      # Output: 'value'
print(getattr(obj, 'missing', 'default'))  # Output: 'default'

id()

Returns the unique identifier for an object (its memory address).

x = 10
print(id(x))  # Output: (unique id for object 10)

len()

Returns the length (number of items) of an object, such as a string, list, or dictionary.

print(len("hello"))   # Output: 5
print(len([1, 2, 3])) # Output: 3

map()

Applies a function to all items in an iterable and returns an iterator of results.

def square(x):
    return x * x

numbers = [1, 2, 3, 4]
squared = map(square, numbers)
print(list(squared))  # Output: [1, 4, 9, 16]

min()

Returns the smallest item in an iterable or the smallest of two or more arguments.

print(min(1, 2, 3))         # Output: 1
print(min([4, 2, 8, 6]))    # Output: 2

pow()

Returns the result of raising the first argument to the power of the second argument (i.e., x**y). Optionally, a third argument can be used for modulus.

print(pow(2, 3))            # Output: 8
print(pow(2, 3, 3))         # Output: 2 (8 % 3)

print()

Outputs text or other data to the console.

print("Hello, World!")  # Output: Hello, World!

setattr()

Sets an attribute of an object to a specified value.

class MyClass:
    pass

obj = MyClass()
setattr(obj, 'attr', 'value')
print(obj.attr)  # Output: 'value'

sorted()

Returns a new sorted list from the elements of an iterable.

print(sorted([3, 1, 2]))        # Output: [1, 2, 3]
print(sorted("hello", reverse=True))  # Output: 'ollhe'

type ()

Returns the type of an object.

print(type(123))    # Output: <class 'int'>
print(type("text")) # Output: <class 'str'>

User defined functions

Basic Function

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

print(greet("Alice"))  # Output: Hello, Alice!

Function with Default Parameters

def multiply(x, y=2):
    return x * y

print(multiply(3))    # Output: 6 (y defaults to 2)
print(multiply(3, 4)) # Output: 12

Function with Multiple Return Values

def divide_and_remainder(x, y):
    return x // y, x % y

quotient, remainder = divide_and_remainder(10, 3)
print(quotient)   # Output: 3
print(remainder)  # Output: 1

Function with Arbitrary Arguments

def print_args(*args):
    for arg in args:
        print(arg)

print_args(1, 2, 3)  # Output: 1 \n 2 \n 3

Function with Keyword Arguments

def describe_person(name, **kwargs):
    print(f"Name: {name}")
    for key, value in kwargs.items():
        print(f"{key}: {value}")

describe_person("Alice", age=30, city="New York")
# Output: Name: Alice \n age: 30 \n city: New York

Recursion

Recursion is a programming technique where a function calls itself to solve a problem. It is a powerful concept for solving problems that can be broken down into smaller, similar subproblems.

A recursive function typically has two main components:

  1. Base Case: The condition under which the recursion stops. It provides an answer without making further recursive calls.

  2. Recursive Case: The part of the function that includes the recursive call. It typically involves breaking down the problem into smaller instances of the same problem.

Syntax:

def recursive_function(parameters):
    if base_case_condition:
        return base_case_result
    else:
        # Recursive case
        return recursive_function(modified_parameters)

Factorial Using recursion

he factorial of a non-negative integer 𝑛 n is the product of all positive integers less than or equal to 𝑛 n. It is often defined recursively as:

  • Factorial of 0 or 1: 1

  • Factorial of n: 𝑛 × factorial of ( 𝑛 − 1 ) n×factorial of (n−1)

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

print(factorial(5))  # Output: 120

The factorial function calculates the factorial of a non-negative integer n using recursion. The function starts with a base case where if n is 0, it returns 1 since the factorial of 0 is 1. For any other positive integer n, the function returns n multiplied by the factorial of n - 1. This recursive call continues until it reaches the base case. For example, calling factorial(5) results in the calculation 5x4x3x2x1, which equals 120.

Anonymous Functions

Anonymous functions are functions that are defined without a name. The most common way to create anonymous functions is by using the lambda keyword. These functions are useful for short-lived operations where defining a full function might be overkill.

Lambda Functions

A lambda function is a small, anonymous function defined using the lambda keyword. Unlike regular functions defined with def, lambda functions are typically used for short, simple operations and are often used as arguments to higher-order functions like map(), filter(), and sorted().

Syntax:

lambda arguments: expression
  • lambda: The keyword to define an anonymous function.

  • arguments: A comma-separated list of parameters.

  • expression: A single expression that the function evaluates and returns

Basic Lambda Function

# Lambda function that adds 10 to the input
add_ten = lambda x: x + 10
print(add_ten(5))  # Output: 15

Lambda with Map()

# Use lambda to double each element in a list
numbers = [1, 2, 3, 4]
doubled = map(lambda x: x * 2, numbers)
print(list(doubled))  # Output: [2, 4, 6, 8]

Lambda with filter()

# Filter out even numbers using lambda
numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers))  # Output: [2, 4]

Lambda with sorted()

# Sort a list of tuples by the second element using lambda
tuples = [(1, 'one'), (3, 'three'), (2, 'two')]
sorted_tuples = sorted(tuples, key=lambda x: x[1])
print(sorted_tuples)  # Output: [(1, 'one'), (2, 'two'), (3, 'three')]

Lambda with inline Operations

# Using lambda directly in a function call
result = (lambda x, y: x * y)(5, 3)
print(result)  # Output: 15

Modules

Python Modules are individual files that contain Python code. They are the smallest unit of code organization and can include functions, classes, and variables.

  • A module is a single Python file (.py) that can be imported into other modules or scripts.

  • The name of the module is the name of the file without the .py extension.

  • Modules allow us to break down our code into manageable parts, making it easier to reuse and maintain.

Creating a Module

Let's create a file named math_operations.py with the following content

# math_operations.py
def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

Using a Module

We can import and use this module in another script

# main.py
import math_operations

result = math_operations.add(5, 3)
print(result)  # Output: 8

Packages

Python Packages are a way of organizing related modules into a directory hierarchy. A package is essentially a directory that contains multiple modules and a special file called init.py.

  • A package is a directory that contains multiple module files and an init.py file.

  • init.py file is executed when the package is imported and can be empty or contain initialization code. It tells Python that the directory should be treated as a package.

  • Packages can contain sub-packages, allowing for a hierarchical organization of modules.

Creating a Package

Let's create the following directory structure

my_package/
├── __init__.py
├── math_operations.py
└── utils.py

init.py can be empty or contain initialization code.

math_operations.py is defined below

def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

utils.py is defined below

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

Using a package

We can import and use modules from this package in another script

# main.py
from my_package import math_operations, utils

result = math_operations.add(5, 3)
print(result)  # Output: 8

message = utils.greet("Alice")
print(message)  # Output: Hello, Alice!

If we want to import everything from math_operations, we can use the following code.

# main.py
from my_package.math_operations import add, subtract

result = add(5, 3)
print(result)  # Output: 8

Exception Handling

Exception handling is a mechanism that allows us to manage and respond to runtime errors in a controlled way. Instead of letting our program crash when an error occurs, we can use exception handling to catch these errors and handle them gracefully.

  • Exception: An error that occurs during the execution of a program, such as dividing by zero or accessing a file that doesn’t exist.

  • Try Block: The block of code where we write the code that might raise an exception.

  • Except Block: The block of code that runs if an exception occurs in the try block. It catches the exception and allows us to handle it.

  • Else Block: (Optional) If no exceptions occur in the try block, the code in the else block is executed.

  • Finally Block: (Optional) This block of code always runs, regardless of whether an exception occurred or not. It is typically used for cleanup actions.

Syntax:

try:
    # Code that may raise an exception
    risky_operation()
except ExceptionType as e:
    # Code to handle the exception
    handle_exception(e)
else:
    # Code that runs if no exception occurs
    no_exception_occurred()
finally:
    # Code that always runs, regardless of exceptions
    cleanup()

Basic Exception Handling

try:
    result = 10 / 2
except ZeroDivisionError:
    print("You can't divide by zero!")
else:
    print(f"Result is {result}")  # Runs if no exception occurs
finally:
    print("This will always be executed.")

The try block contains code that might raise a ZeroDivisionError. The except block catches this specific error and prints a message.

The else block runs if no exceptions are raised in the try block, and the finally block always runs regardless of whether an exception occurred or not.

Built-in Exceptions

Python comes with a set of built-in exceptions that represent various types of errors.

Some of the most common used built-in exceptions are:

  • Exception: The base class for all built-in exceptions. It is used to catch any exception. We can use this as base class.

  • ZeroDivisionError: Raised when a division by zero occurs.

    Example: x = 1 / 0

  • ValueError: Raised when a function receives an argument of the right type but inappropriate value.

    Example: int("string")

  • TypeError: Raised when an operation or function is applied to an object of inappropriate type.

    Example: "string" + 1

  • IndexError: Raised when a sequence subscript is out of range.

    Example: my_list = [1, 2]; my_list[3]

  • KeyError: Raised when a dictionary key is not found.

    Example: my_dict = {'a': 1}; my_dict['b']

  • FileNotFoundError: Raised when trying to open a file that does not exist.

    Example: open('non_existent_file.txt')

  • AttributeError: Raised when an invalid attribute reference is made.

    Example: 'string'.non_existent_method()

  • IOError: Raised when an I/O operation fails (e.g., file operations).

    Example: This exception is now an alias for OSError.

  • OSError: Raised for operating system-related errors.

    Example: os.remove('non_existent_file.txt')

User-defined Exceptions

We can create our own custom exceptions by subclassing the built-in Exception class or any of its subclasses. This allows us to define exceptions specific to our application's needs.

# Define Exception class
class MyCustomError(Exception):
    def __init__(self, message):
        self.message = message
        super().__init__(self.message)

# Raise custom exception
def do_something(value):
    if value < 0:
        raise MyCustomError("Value must be non-negative.")
    return value * 2

try:
    print(do_something(-1))
except MyCustomError as e:
    print(e)

MyCustomError class inherits from Exception and takes a custom error message. The do_something function raises MyCustomError if the value is negative. The try block catches this exception and prints the error message.

Output formatting

Output formatting in Python is crucial for presenting data in a clear and readable manner. Python provides several methods to format output, each with its own strengths and use cases.

Using the print() Function

Basic Formatting

print("Hello, World!")

Separator and End Arguments

print("Hello", "World", sep=", ", end="!\n")
  • sep: Separator between items.

  • end: String appended after the last item

String Formatting with str.format()

Basic

name = "Alice"
age = 30
print("Name: {}, Age: {}".format(name, age))

Positional and Keyword Arguments

print("{0} is {1} years old.".format(name, age))
print("{name} is {age} years old.".format(name="Bob", age=25))

Formatting with Alignment, Width, and Precision

# Alignment
print("{:<10} | {:>10}".format("left", "right"))

# Width and Precision
pi = 3.141592653589793
print("Pi to 2 decimal places: {:.2f}".format(pi))

f-Strings (Formatted String Literals)

Basic

name = "Alice"
age = 30
print(f"Name: {name}, Age: {age}")

Expression Evaluation

import math
print(f"The square root of 2 is approximately {math.sqrt(2):.2f}")

Alignment and Padding

print(f"{'left':<10} | {'right':>10}")

Formatting for Data Tables

The tabulate library provides a convenient way to format tabular data.

from tabulate import tabulate

data = [["Name", "Age"], ["Alice", 30], ["Bob", 25]]
print(tabulate(data, headers='firstrow', tablefmt='grid'))

JSON Output Formatting

Pretty printing Json Data

import json

data = {"name": "Alice", "age": 30, "city": "New York"}
print(json.dumps(data, indent=4))

Code Writing Style

Coding styles help ensure that code is readable, maintainable, and consistent across different projects and teams.

The most commonly followed coding style guide is PEP 8, which provides conventions for writing Python code.

Indentation: Four spaces per indentation level.

def example_function():
    if True:
        print("Hello, World!")

Line Length: Limit all lines to 79 characters.

# Good
def some_function_with_a_reasonably_long_name_and_parameters(x, y, z):
    return x + y + z

# Bad (exceeds 79 characters)
def some_function_with_a_reasonably_long_name_and_parameters(x, y, z, a, b, c, d, e, f):
    return x + y + z + a + b + c + d + e + f

Blank Lines:

  • Function and Class Definitions*: Separate top-level function and class definitions with two blank lines.*

  • Method Definitions*: Separate method definitions within a class with one blank line.*

class MyClass:
    def __init__(self, value):
        self.value = value

    def get_value(self):
        return self.value

Imports*: Imports should be on separate lines and grouped in three categories: standard library imports, related third-party imports, and local application/library-specific imports.*

import os
import sys

import requests

from mymodule import myfunction

Naming Conventions

  • Variables*: Use snake_case for variables and functions.*

  • Classes*: Use CamelCase for class names.*

  • Constants*: Use UPPER_CASE for constants*

def calculate_area(radius):
    PI = 3.14159
    return PI * radius * radius

class Circle:
    pass

Whitespace: Avoid extra spaces in expressions and statements.

# Good
total = (x + y) * z

# Bad
total = ( x +      y ) *   z

Comments

  • Block Comments*: Use block comments to describe sections of code and place them above the code they describe.*

  • Inline Comments*: Use inline comments sparingly and place them on the same line as the statement they comment on.*

# This function calculates the area of a circle
def calculate_area(radius):
    PI = 3.14159  # Define the value of PI
    return PI * radius * radius

Docstrings: Write docstrings for all public modules, functions, classes, and methods to describe their purpose and usage. Use triple double quotes (""") for docstrings.

def add(a, b):
    """
    Add two numbers and return the result.

    Parameters:
    a (int): The first number.
    b (int): The second number.

    Returns:
    int: The sum of a and b.
    """
    return a + b

Exception Handling: Prefer try-except blocks for handling exceptions and avoid catching generic exceptions unless absolutely necessary.

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")

Function and Variable Annotations: Provide type hints for function arguments and return values.

def multiply(x: int, y: int) -> int:
    return x * y

Programming Paradigm

Programming paradigms are approaches or styles of programming that provide different methodologies for solving problems and structuring code. As Python is a multi-paradigm language, it supports several programming paradigms.

Object Oriented Programming

Object Oriented Programming (OOP) is a paradigm based on the concept of "objects," which contain data and code. The data is represented as fields (attributes), and the code is represented as methods (functions).

Features

  • Classes and Objects: In Python, a class is a blueprint for creating objects. It encapsulates data (attributes) and behavior (methods) into a single entity. Objects are instances of classes.

  • Inheritance: We can create new classes based on existing ones, inheriting attributes and methods.

  • Encapsulation: OOP promotes bundling of data and methods into a single unit (class) and controlling access to that data.

  • Polymorphism: Objects of different classes can be treated as objects of a common superclass. Methods can have the same name but behave differently based on the object’s class.

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

    def speak(self):
        raise NotImplementedError

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

dog = Dog("Buddy")
print(dog.speak())  # Output: Woof!

Procedural Programming

Procedural Programming or imperative programming is based on the concept of procedure calls. Programs are structured as sequences of instructions (procedures or functions) that operate on data.

Features:

  • Functions: In Python, functions are blocks of reusable code that perform a specific task. They help in organizing code, avoiding repetition, and improving readability.

  • State Management: The state of the program is managed through variables and can be modified by functions.

  • Control Flow: Control structures such as loops and conditionals manage the execution flow.

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

print(greet("Alice"))  # Output: Hello, Alice!

Functional Programming

Functional Programming treats computation as the evaluation of mathematical functions and avoids changing state or mutable data.

Features:

  • First-Class Functions: Functions can be passed as arguments, returned from other functions, and assigned to variables.

  • Immutability: Data is not modified after creation; instead, new data is produced.

  • Higher-Order Functions: Functions that accept other functions as arguments or return functions.

  • Pure Functions: Functions that do not have side effects and return the same result given the same input.

    • By having no side effects, it means that a pure function does not alter any state outside its scope or interact with the outside world. It does not modify global variables, change the value of input parameters, or perform I/O operations.
def add(x, y):
    return x + y

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

print(apply_function(add, 5, 3))  # Output: 8

Structured Programming

Structured programming focuses on breaking down a program into smaller, manageable sections using structured control flow constructs.

Features:

  • Top-Down Design: Decomposing a problem into smaller sub-problems.

  • Control Structures: Uses loops and conditionals to manage execution flow.

  • Modularity: Code is organized into functions or procedures.

def calculate_area(radius):
    return 3.14 * radius * radius

def print_area(radius):
    area = calculate_area(radius)
    print(f"Area: {area}")

print_area(5)  # Output: Area: 78.5

Reflective Programming

Reflective programming involves a program's ability to inspect and modify its own structure and behavior at runtime.

Features:

  • Introspection: Ability to examine the types or properties of objects at runtime.

  • Meta-Programming: Writing code that manipulates code (e.g., creating or modifying classes).

  • Dynamic Behavior: Modifying or extending behavior during execution.

class Example:
    def __init__(self, value):
        self.value = value

example = Example(10)
print(dir(example))  # Output: List of attributes and methods

setattr(example, 'new_attr', 20)
print(example.new_attr)  # Output: 20

Declarative Programming

Declarative programming focuses on what should be done rather than how to do it. Declarative programming is less about control flow and more about specifying properties and constraints.

Features:

  • Expressiveness: Describe what the program should accomplish.

  • Abstraction: Abstracts away the control flow and implementation details.

  • Examples: SQL for querying databases, regular expressions for pattern matching.

# Using a list comprehension to declaratively generate a list
squares = [x * x for x in range(10)]
print(squares)  # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

After Words

I hope this comprehensive tutorial was helpful to you. Please feel free to reach out in case of any doubts or errors.

ML-AI

Part 3 of 5

We will start the discussion from mathematics behind machine learning and transition to Machine learning concepts and then finally to implementation of ML models.

Up next

Why learn programming in GPT era?

Last updated: 10 Aug, 2024