Python

An introduction to Python programming.

Python is a high-level, interpreted programming language known for its simplicity and readability. It is widely used for web development, data analysis, artificial intelligence, scientific computing, and more.

Installing Python using a Version Manager

To install Python using a version manager, follow these steps based on your operating system.

Windows (using pyenv-win)

  1. Open PowerShell as Administrator.

  2. Install pyenv-win:

    Invoke-WebRequest -UseBasicParsing https://pyenv.run | Invoke-Expression
    
  3. Restart your terminal and install Python:

    pyenv install 3.x.x  # Replace with the desired version
    pyenv global 3.x.x
    

macOS/Linux (using pyenv)

  1. Open Terminal.

  2. Install pyenv dependencies:

    curl https://pyenv.run | bash
    
  3. Follow the prompts to add pyenv to your .bashrc or .zshrc:

    export PATH="$HOME/.pyenv/bin:$PATH"
    eval "$(pyenv init --path)"
    eval "$(pyenv init -)"
    
  4. Restart your terminal, then install Python:

    pyenv install 3.x.x  # Replace with the desired version
    pyenv global 3.x.x
    

Verifying Installation

Check the installed Python version:

python --version

Managing Python Versions and Virtual Environments (macOS/Linux) using pyenv

Once pyenv is installed, follow these steps to install Python versions and manage virtual environments on Unix-based systems.

Installing Different Python Versions

  1. List all available Python versions:

    pyenv install --list
    
  2. Install a specific Python version:

    pyenv install 3.x.x  # Replace with the desired version
    
  3. Set the global default Python version:

    pyenv global 3.x.x  # Replace with the installed version you want as default
    
  4. Set the Python version for the current shell session only:

    pyenv shell 3.x.x  # Replace with the version you want to use
    

Creating and Managing Virtual Environments

  1. Create a new virtual environment:

    pyenv virtualenv 3.x.x myenv  # Replace with the desired Python version and environment name
    
  2. Activate the virtual environment:

    pyenv activate myenv
    
  3. Deactivate the virtual environment:

    pyenv deactivate
    
  4. List all available environments:

    pyenv virtualenvs
    

Installing Dependencies in a Virtual Environment

  1. Activate the desired environment:

    pyenv activate myenv
    
  2. Install packages:

    pip install package_name
    
  3. To install all dependencies from a requirements.txt file:

    pip install -r requirements.txt
    

Saving and Recreating the Virtual Environment

  1. To save the state of the virtual environment and create a requirements.txt file:

    pip freeze > requirements.txt
    
  2. To recreate the environment on another system or after a fresh setup:

    pip install -r requirements.txt
    

Managing Python Versions and Environments with pyenv and Poetry (macOS/Linux)

Use pyenv to switch Python versions and Poetry to manage dependencies and virtual environments. Here's how to use them together, including how to handle situations where the Python version is not installed.

Setting Python Version with pyenv

  1. List all available Python versions:

    pyenv install --list
    
  2. Install the desired Python version:

    pyenv install 3.x.x  # Replace with the desired version
    
  3. Set the global or local Python version for the project:

    Global:

    pyenv global 3.x.x
    

    Local (for the current project folder):

    pyenv local 3.x.x
    

Using Poetry to Manage the Environment

  1. After switching to the desired Python version using pyenv, create a new Poetry project:

    poetry new myproject
    cd myproject
    
  2. Configure Poetry to use the specific Python version set by pyenv:

    poetry env use 3.x.x
    

    If the Python version is not installed, you can manually install it using pyenv or your system's package manager:

    pyenv install 3.x.x
    
  3. To check which Python version is being used:

    poetry env info
    

Installing Dependencies with Poetry

  1. Install a package:

    poetry add package_name
    
  2. Install dependencies listed in pyproject.toml:

    poetry install
    

Saving and Recreating the Environment

  1. To export the environment's dependencies to poetry.lock (happens automatically when adding packages):

    poetry lock
    
  2. To recreate the environment in another system or after a fresh setup:

    poetry install
    

Switching Between Python Versions

  1. Use pyenv to switch Python versions:

    pyenv shell 3.x.x  # Replace with the desired version
    
  2. Update the Poetry environment to use the new Python version:

    poetry env use 3.x.x
    
  3. If the specified Python version in pyproject.toml is not installed, Poetry will give an error. You can fix this by installing the missing version with pyenv:

    pyenv install 3.x.x
    

Hello World in Python

The classic first program in any language is printing "Hello, World!" to the screen.

print("Hello, World!")

This will output:

Hello, World!

Getting User Input

In Python, you can use the input() function to get input from the user.

name = input("Enter your name: ")
print(f"Hello, {name}!")

This will prompt the user for their name and then greet them.

Enter your name: John
Hello, John!

Printing Output

Python's print() function can display multiple variables and values.

age = 30
print("I am", age, "years old.")

This will output:

I am 30 years old.

You can also format the output with f-strings (introduced in Python 3.6+):

age = 30
print(f"I am {age} years old.")

Operators in Python

Python supports a variety of operators for performing different operations like arithmetic, comparison, logical, and bitwise operations.

1. Arithmetic Operators

Arithmetic operators are used for basic mathematical operations.

OperatorDescriptionExample
+Addition3 + 25
-Subtraction5 - 32
*Multiplication4 * 28
/Division8 / 24.0
%Modulus (Remainder)5 % 21
**Exponentiation2 ** 38
//Floor Division5 // 22

2. Comparison Operators

Comparison operators are used to compare two values and return a boolean result (True or False).

OperatorDescriptionExample
==Equal to3 == 3True
!=Not equal to3 != 2True
>Greater than5 > 3True
<Less than2 < 5True
>=Greater than or equal to3 >= 3True
<=Less than or equal to2 <= 5True

3. Logical Operators

Logical operators are used to combine conditional statements.

OperatorDescriptionExample
andReturns True if both statements are TrueTrue and FalseFalse
orReturns True if one of the statements is TrueTrue or FalseTrue
notReverses the result, returns False if the result is Truenot TrueFalse

4. Assignment Operators

Assignment operators are used to assign values to variables.

OperatorDescriptionExample
=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

5. Bitwise Operators

Bitwise operators operate on the binary representations of integers.

OperatorDescriptionExample
&AND5 & 31
``OR
^XOR5 ^ 36
~NOT~5-6
<<Left shift2 << 14
>>Right shift4 >> 12

6. Membership and Identity Operators

Membership:

  • in: Checks if a value is present in a sequence (e.g., list, tuple).
  • not in: Checks if a value is not present in a sequence.

Example:

x = [1, 2, 3]
print(2 in x)  # Output: True
print(4 not in x)  # Output: True

Identity:

  • is: Checks if two variables refer to the same object.
  • is not: Checks if two variables do not refer to the same object.

Example:

a = [1, 2, 3]
b = a
print(a is b)  # Output: True

Summary of Python Operators:

  • Arithmetic operators: Perform basic math operations like addition and subtraction.
  • Comparison operators: Compare two values and return boolean results.
  • Logical operators: Combine conditional statements.
  • Bitwise operators: Perform operations on binary representations of integers.
  • Assignment operators: Assign values to variables and modify them.
  • Membership and Identity operators: Check membership in sequences and identity of objects.

Data Types in Python

Python has several built-in data types. Here are the most common ones:

1. Integers (int)

Whole numbers, positive or negative.

x = 10
print(type(x))  # Output: <class 'int'>

2. Floating Point Numbers (float)

Numbers with decimal points.

x = 3.14
print(type(x))  # Output: <class 'float'>

3. Strings (str)

A sequence of characters, enclosed in single or double quotes.

x = "Hello, World!"
print(type(x))  # Output: <class 'str'>

4. Booleans (bool)

Logical values, either True or False.

x = True
print(type(x))  # Output: <class 'bool'>

5. Lists (list)

Ordered, mutable collections of values.

x = [1, 2, 3, 4]
print(type(x))  # Output: <class 'list'>

6. Dictionaries (dict)

Key-value pairs, unordered and mutable.

x = {"name": "John", "age": 30}
print(type(x))  # Output: <class 'dict'>

7. Tuples (tuple)

Ordered, immutable collections of values.

x = (1, 2, 3)
print(type(x))  # Output: <class 'tuple'>

8. Sets (set)

Unordered collections of unique values.

x = {1, 2, 3}
print(type(x))  # Output: <class 'set'>

Summary:

  • Python provides several built-in data types such as integers, floats, strings, booleans, lists, dictionaries, tuples, and sets.
  • You can use the input() function to get user input, and the print() function to output information to the console.

Additional Data Types in Python

Beyond the basic types like integers, floats, strings, and lists, Python provides other useful data types that may be less commonly discussed but are equally important.

1. NoneType (None)

Represents the absence of a value or a null value.

x = None
print(type(x))  # Output: <class 'NoneType'>

None is often used as a placeholder or default value in Python functions and objects.

2. Bytes (bytes)

Immutable sequences of bytes, often used for binary data or encoding.

x = b"hello"
print(type(x))  # Output: <class 'bytes'>

Bytes are useful when working with binary data, such as files or network transmissions.

3. Bytearray (bytearray)

Similar to bytes, but mutable (you can change elements).

x = bytearray(b"hello")
x[0] = 72  # Change the first element
print(x)  # Output: bytearray(b'Hello')

Use bytearray when you need to modify a sequence of bytes.

4. Memoryview (memoryview)

A way to access the memory of an object without copying it, typically used with bytes and bytearray.

x = memoryview(b"hello")
print(x)  # Output: <memory at 0x...>

memoryview can be useful for performance optimization when working with large binary data.

5. Range (range)

Represents a sequence of numbers, commonly used in loops.

x = range(10)
print(type(x))  # Output: <class 'range'>

range is useful when you want to iterate over a sequence of numbers without creating a list in memory.

6. Complex Numbers (complex)

Numbers with a real and imaginary part.

x = 3 + 5j
print(type(x))  # Output: <class 'complex'>

Complex numbers are useful in mathematical and scientific computations where imaginary numbers are required.

7. Frozenset (frozenset)

An immutable version of a set. Once created, elements cannot be added or removed.

x = frozenset([1, 2, 3])
print(type(x))  # Output: <class 'frozenset'>

frozenset is useful when you need an immutable set, typically as keys in dictionaries or elements in other sets.

Summary of Additional Data Types:

  • NoneType: Represents the absence of a value.
  • bytes and bytearray: For handling binary data, with bytearray being mutable.
  • memoryview: Efficient memory access without copying.
  • range: A sequence of numbers, often used in loops.
  • complex: For working with complex numbers.
  • frozenset: Immutable sets.

Useful Built-in Functions in Python

Python provides a variety of built-in functions that make development easier and more efficient. Here are some of the most commonly used ones:

1. callable()

Checks if an object can be called like a function. Functions, methods, and objects with a __call__() method return True.

def my_function():
    return "Hello"

print(callable(my_function))  # Output: True

2. enumerate()

Adds a counter to an iterable (like a list or tuple) and returns it as an enumerate object, which you can loop over to get both the index and the value.

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

Output:

0 apple
1 banana
2 cherry

3. zip()

Combines two or more iterables (such as lists or tuples) into pairs or tuples, returning them as a zip object.

names = ['Alice', 'Bob', 'Charlie']
scores = [85, 90, 95]
combined = zip(names, scores)

for name, score in combined:
    print(f"{name}: {score}")

Output:

Alice: 85
Bob: 90
Charlie: 95

4. map()

Applies a given function to each item of an iterable (like a list) and returns a map object.

numbers = [1, 2, 3, 4]
doubled = map(lambda x: x * 2, numbers)

print(list(doubled))  # Output: [2, 4, 6, 8]

5. filter()

Filters elements from an iterable based on a function that returns True or False.

numbers = [1, 2, 3, 4, 5, 6]
even_numbers = filter(lambda x: x % 2 == 0, numbers)

print(list(even_numbers))  # Output: [2, 4, 6]

6. sorted()

Returns a sorted list of the elements in an iterable.

numbers = [3, 1, 4, 1, 5, 9]
print(sorted(numbers))  # Output: [1, 1, 3, 4, 5, 9]

You can also sort based on a custom key:

words = ['banana', 'apple', 'cherry']
print(sorted(words, key=len))  # Output: ['apple', 'banana', 'cherry']

7. any()

Returns True if any element in the iterable is True. It stops at the first True it finds.

numbers = [0, 1, 2, 3]
print(any(numbers))  # Output: True

8. all()

Returns True if all elements in the iterable are True. If any element is False, it returns False.

numbers = [1, 2, 3, 4]
print(all(numbers))  # Output: True

9. max() and min()

Returns the largest and smallest items in an iterable, respectively.

numbers = [10, 20, 30, 40]
print(max(numbers))  # Output: 40
print(min(numbers))  # Output: 10

10. sum()

Returns the sum of all elements in an iterable.

numbers = [10, 20, 30]
print(sum(numbers))  # Output: 60

11. len()

Returns the length (number of items) of an object like a list, string, tuple, etc.

items = [1, 2, 3, 4, 5]
print(len(items))  # Output: 5

12. type()

Returns the type of an object.

x = 42
print(type(x))  # Output: <class 'int'>

13. isinstance()

Checks if an object is an instance or subclass of a class or a tuple of classes.

x = 42
print(isinstance(x, int))  # Output: True

Summary of Useful Built-in Functions:

  • callable(): Check if an object can be called.
  • enumerate(): Add a counter to an iterable.
  • zip(): Combine iterables into tuples.
  • map(): Apply a function to each item in an iterable.
  • filter(): Filter items from an iterable based on a function.
  • sorted(): Return a sorted list.
  • any(): Check if any item in an iterable is True.
  • all(): Check if all items in an iterable are True.
  • max() and min(): Find the largest and smallest elements.
  • sum(): Sum up elements in an iterable.
  • len(): Find the length of an object.
  • type(): Get the type of an object.
  • isinstance(): Check if an object is an instance of a class.

Special Note: Iterables and Sequence Types in Python

What is an Iterable?

An iterable is any Python object capable of returning its elements one at a time. Iterables can be used in a for loop or with functions like map(), filter(), and zip(). Common iterable types in Python include:

  • Lists (list)
  • Tuples (tuple)
  • Strings (str)
  • Dictionaries (dict)
  • Sets (set)
  • Range objects (range)
  • Files (open file objects)

Example of an Iterable:

numbers = [1, 2, 3, 4]
for number in numbers:
    print(number)

In the above example, the list numbers is iterable because you can loop through its elements.

Sequence Types

A sequence is a special type of iterable that has a specific order to its elements and supports element access using integer indices. Sequences include:

  • Lists (list)
  • Tuples (tuple)
  • Strings (str)
  • Range objects (range)
  • Byte Array(bytearray)

Properties of Sequence Types:

  • Indexing: Access elements using indices (e.g., my_list[0]).
  • Slicing: Retrieve a slice (sublist) from the sequence using slicing (e.g., my_list[1:3]).
  • Reversibility: Sequence types can often be reversed using methods like reversed().

Example of a Sequence (List):

fruits = ['apple', 'banana', 'cherry']
print(fruits[0])  # Output: apple

# Slicing example
print(fruits[1:])  # Output: ['banana', 'cherry']

Iterators vs. Iterables

  • An iterable is any object capable of returning an iterator (e.g., a list).
  • An iterator is an object that represents a stream of data, produced one element at a time by calling next() on it.

Example of Creating an Iterator from an Iterable:

numbers = [1, 2, 3]
iterator = iter(numbers)

print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3

Mutable vs. Immutable Sequence Types

  • Mutable sequences: These sequences can be modified after their creation (e.g., lists, bytearrays).
  • Immutable sequences: These sequences cannot be modified (e.g., strings, tuples, bytes).

Mutable Sequence Example:

my_list = [1, 2, 3]
my_list[0] = 10
print(my_list)  # Output: [10, 2, 3]

Immutable Sequence Example:

my_tuple = (1, 2, 3)
# Attempting to modify it will raise an error
# my_tuple[0] = 10  # Raises TypeError

Common Functions that Work on Iterables:

Many Python built-in functions operate on iterables and sequences:

  • len(): Returns the length of an iterable.
  • max()/min(): Returns the maximum or minimum value in a sequence.
  • sorted(): Returns a sorted sequence from an iterable.
  • sum(): Returns the sum of all elements in a sequence.

Summary:

  • Iterable: Any object that can return its elements one at a time (used in loops).
  • Sequence: A type of iterable that has order and supports indexing and slicing.
  • Iterator: An object that represents a stream of data from an iterable, produced one element at a time using next().
  • Mutable vs Immutable Sequences: Lists are mutable, while tuples and strings are immutable.

Strings in Python

A string in Python is a sequence of characters enclosed within single ('), double ("), or triple quotes (''', """). Strings are immutable, meaning once created, their content cannot be changed.

Example:

my_string = "Hello, World!"
print(my_string)

String Indexing and Slicing

Strings are indexed starting from 0, and you can access individual characters or substrings using slicing.

my_string = "Python"

# Accessing individual characters
print(my_string[0])  # Output: P
print(my_string[-1])  # Output: n

# Slicing the string
print(my_string[1:4])  # Output: yth

Common String Operations

Here are some common string operations:

  1. Concatenation: Use the + operator to concatenate strings.

    str1 = "Hello"
    str2 = "World"
    result = str1 + " " + str2
    print(result)  # Output: Hello World
    
  2. Repetition: Use the * operator to repeat a string multiple times.

    print("Hello" * 3)  # Output: HelloHelloHello
    
  3. Length: Use len() to get the length of a string.

    my_string = "Hello"
    print(len(my_string))  # Output: 5
    
  4. Membership: Use in to check if a substring is present in a string.

    print("Py" in "Python")  # Output: True
    

String Formatting

String formatting allows you to insert variables or expressions inside strings. There are several ways to format strings in Python:

1. Using format():

name = "John"
age = 30
print("My name is {} and I am {} years old.".format(name, age))
# Output: My name is John and I am 30 years old.

2. Using f-strings (Python 3.6+):

F-strings are a modern, concise way to format strings.

name = "John"
age = 30
print(f"My name is {name} and I am {age} years old.")
# Output: My name is John and I am 30 years old.

3. Using Percent Formatting (Old-style):

This is an older way of formatting strings, using % symbols.

name = "John"
age = 30
print("My name is %s and I am %d years old." % (name, age))
# Output: My name is John and I am 30 years old.

String Methods

Python provides several built-in methods for string manipulation. Here are some commonly used ones:

  1. upper(): Converts a string to uppercase.

    print("hello".upper())  # Output: HELLO
    
  2. lower(): Converts a string to lowercase.

    print("HELLO".lower())  # Output: hello
    
  3. strip(): Removes whitespace from the beginning and end of a string.

    my_string = "  hello  "
    print(my_string.strip())  # Output: hello
    
  4. replace(): Replaces all occurrences of a substring with another.

    print("Hello, World!".replace("World", "Python"))  # Output: Hello, Python!
    
  5. split(): Splits a string into a list of substrings based on a delimiter.

    my_string = "apple,banana,cherry"
    print(my_string.split(","))  # Output: ['apple', 'banana', 'cherry']
    
  6. join(): Joins a list of strings into a single string with a specified separator.

    fruits = ['apple', 'banana', 'cherry']
    print(", ".join(fruits))  # Output: apple, banana, cherry
    
  7. startswith() and endswith(): Check if a string starts or ends with a specified substring.

    print("Python".startswith("Py"))  # Output: True
    print("Python".endswith("on"))    # Output: True
    
  8. find(): Returns the index of the first occurrence of a substring, or -1 if not found.

    print("Hello, World!".find("World"))  # Output: 7
    

Multiline Strings

You can create multiline strings using triple quotes (''' or """).

my_string = """This is a 
multiline string"""
print(my_string)

Escape Characters

Escape characters allow you to include special characters in strings, such as newlines or tabs.

  • \n: Newline
  • \t: Tab
  • \\: Backslash
print("Hello\nWorld")  # Output: Hello (newline) World
print("Name:\tJohn")   # Output: Name:    John

Summary:

  • Strings are immutable sequences of characters in Python.
  • You can use a variety of methods for string manipulation like upper(), lower(), replace(), split(), and join().
  • String formatting can be done with format(), f-strings, or percent formatting.
  • Strings can be indexed, sliced, and concatenated.

Flow Control Statements in Python

Flow control statements allow you to change the execution order of statements based on conditions, loops, or exceptions. Here are the main flow control statements in Python.

1. if, elif, and else

The if statement is used to execute a block of code only if a condition is True. You can also use elif for additional conditions, and else to handle the case where none of the conditions are true.

Syntax:

if condition:
    # Code block for True condition
elif another_condition:
    # Code block for another True condition
else:
    # Code block if all conditions are False

Example:

age = 18
if age < 18:
    print("You are a minor.")
elif age == 18:
    print("You just became an adult!")
else:
    print("You are an adult.")

2. for Loop

The for loop is used to iterate over an iterable (e.g., list, tuple, string) and execute a block of code for each item in the iterable.

Syntax:

for variable in iterable:
    # Code block

Example:

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

Output:

apple
banana
cherry

3. while Loop

The while loop runs as long as a condition is True. Be careful with while loops to avoid infinite loops.

Syntax:

while condition:
    # Code block

Example:

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

Output:

0
1
2
3
4

4. break Statement

The break statement is used to exit a loop prematurely when a certain condition is met.

Example:

for i in range(10):
    if i == 5:
        break  # Exit the loop
    print(i)

Output:

0
1
2
3
4
count = 0
while True:
    print(count)
    count += 1
    if count == 5:
        break  # Exits the while loop when count reaches 5

5. continue Statement

The continue statement skips the current iteration of a loop and moves to the next one.

Example:

for i in range(5):
    if i == 2:
        continue  # Skip the rest of this loop when i == 2
    print(i)

Output:

0
1
3
4

6. pass Statement

The pass statement is a placeholder that does nothing. It's used where a statement is syntactically required but you don't want to perform any action.

Example:

for i in range(5):
    if i == 3:
        pass  # Do nothing for this iteration
    print(i)

Output:

0
1
2
3
4

7. try, except, finally (Exception Handling)

Python uses try and except blocks to handle exceptions (errors). You can use the finally block to execute code regardless of whether an exception occurs or not.

Syntax:

try:
    # Code that may raise an exception
except SomeError:
    # Code to handle the exception
finally:
    # Code that will always run

Example:

try:
    number = int(input("Enter a number: "))
except ValueError:
    print("That's not a valid number.")
finally:
    print("This will always run.")

8. else in Loops

The else block in a loop runs only when the loop completes normally, without encountering a break statement.

Example:

for i in range(5):
    if i == 2:
        break
else:
    print("Loop completed without break")

Output:

(No output, as the loop breaks at i == 2)

9. match Statement (Python 3.10+)

The match statement is similar to a switch-case statement found in other languages. It checks the value of a variable against multiple patterns.

Syntax:

match variable:
    case pattern_1:
        # Code block
    case pattern_2:
        # Code block
    case _: 
        # Default code block, will match any wildcard that does not match previous cases

Example:

def check_day(day):
    match day:
        case "Monday":
            print("Start of the workweek.")
        case "Friday":
            print("Almost weekend!")
        case _:
            print("It's a regular day.")

check_day("Friday")

Output:

Almost weekend!

Summary of Flow Control Statements:

  • if, elif, else: Conditional branching.
  • for loop: Iterates over sequences.
  • while loop: Loops based on a condition.
  • break: Exits the loop.
  • continue: Skips the current iteration.
  • pass: Does nothing (used as a placeholder).
  • try, except, finally: Handles exceptions.
  • match: Pattern matching (Python 3.10+).

Examples of range() in Python

The range() function generates a sequence of numbers. It's commonly used in loops. The syntax is:

range(start, stop, step)
  • start: The starting number (optional, defaults to 0).
  • stop: The stopping number (required, the range stops just before this number).
  • step: The increment (optional, defaults to 1).

Example 1: Basic Range

If you provide just the stop value, range() generates numbers from 0 up to (but not including) stop.

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

Output:

0
1
2
3
4

Example 2: Range with Start and Stop

You can specify both the start and stop values.

for i in range(2, 6):
    print(i)

Output:

2
3
4
5

Example 3: Range with a Step

The step parameter lets you control the interval between numbers.

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

Output:

0
2
4
6
8

Example 4: Range with Negative Step

You can use a negative step to generate numbers in reverse order.

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

Output:

10
8
6
4
2

Example 5: Using Range with list()

You can convert a range object to a list using list().

numbers = list(range(5))
print(numbers)  # Output: [0, 1, 2, 3, 4]

Example 6: Looping Over Indices with range()

You can use range() to loop through the indices of a list.

fruits = ['apple', 'banana', 'cherry']
for i in range(len(fruits)):
    print(f"Index {i}: {fruits[i]}")

Output:

Index 0: apple
Index 1: banana
Index 2: cherry

Example 7: Using Range in Reverse

You can use range() to generate numbers in reverse order by specifying a negative step.

for i in range(5, 0, -1):
    print(i)

Output:

5
4
3
2
1

Summary of range():

  • range(stop): Generates numbers from 0 to stop - 1.
  • range(start, stop): Generates numbers from start to stop - 1.
  • range(start, stop, step): Generates numbers from start to stop - 1, incrementing by step.
  • Negative step: Generates numbers in reverse order.

Exceptions in Python

Exceptions in Python are errors that occur during the execution of a program. When an exception occurs, Python stops executing the code and raises an error message unless the exception is caught and handled. Python provides various ways to handle exceptions to prevent the program from crashing.

1. try, except, and finally

The try block lets you test a block of code for errors, while the except block lets you handle the exception. The finally block lets you execute code, regardless of whether an exception was raised or not.

Example:

try:
    # Code that may raise an exception
    number = int(input("Enter a number: "))
except ValueError:
    # Code to handle the exception
    print("That's not a valid number.")
finally:
    # Code that will always run
    print("End of the program.")

2. Catching Specific Exceptions

You can catch specific exceptions by specifying the exception type in the except block.

Example:

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

Output:

You cannot divide by zero!

3. Catching Multiple Exceptions

You can catch multiple exceptions by listing them as a tuple in a single except block, or by using multiple except blocks.

Example 1: Catch Multiple Exceptions in One Block

try:
    x = int("hello")
except (ValueError, TypeError):
    print("A value or type error occurred.")

Example 2: Use Multiple except Blocks

try:
    x = int("hello")
except ValueError:
    print("A ValueError occurred.")
except TypeError:
    print("A TypeError occurred.")

4. Using else with try

The else block executes if the try block does not raise any exceptions.

Example:

try:
    number = int(input("Enter a valid number: "))
except ValueError:
    print("Invalid number entered.")
else:
    print(f"Your number is {number}.")

5. The finally Block

The finally block always executes, regardless of whether an exception was raised or not. It's useful for cleanup actions, like closing files or releasing resources.

Example:

try:
    file = open("somefile.txt", "r")
except FileNotFoundError:
    print("File not found.")
finally:
    print("Cleaning up...")
    # Code to close the file or clean resources

6. Raising Exceptions

You can manually raise exceptions in Python using the raise keyword.

Example:

x = -5
if x < 0:
    raise ValueError("x cannot be negative")

This will raise a ValueError with the message "x cannot be negative".

7. Creating Custom Exceptions

You can define your own exception classes by inheriting from Python's built-in Exception class.

Example:

class CustomError(Exception):
    pass

def check_positive_number(number):
    if number < 0:
        raise CustomError("This is a custom error: Negative number not allowed!")

try:
    check_positive_number(-10)
except CustomError as e:
    print(e)

Output:

This is a custom error: Negative number not allowed!

### Catching Any Exception in Python

You can catch any exception in Python using a general `except` block with `Exception`:

```python
try:
    # Code that may raise any exception
    risky_operation()
except Exception as e:
    print(f"An error occurred: {e}")

Best Practices

  • Use specific exceptions where possible for better error handling.
  • Exception catches most exceptions, including built-in ones like ValueError, TypeError, etc.
  • Use finally to always execute cleanup code, regardless of whether an exception occurs.

### Common Built-in Exceptions in Python

- **`ValueError`**: Raised when a built-in operation or function receives an argument of the correct type but an inappropriate value.
- **`TypeError`**: Raised when an operation or function is applied to an object of inappropriate type.
- **`KeyError`**: Raised when a dictionary key is not found.
- **`IndexError`**: Raised when a sequence index is out of range.
- **`FileNotFoundError`**: Raised when trying to open a file that does not exist.
- **`ZeroDivisionError`**: Raised when dividing by zero.
- **`AttributeError`**: Raised when an attribute reference or assignment fails.
- **`IOError`**: Raised when an input/output operation fails.

### Summary:

- **`try` and `except`**: Handle exceptions and prevent program crashes.
- **`else`**: Runs if no exceptions are raised in the `try` block.
- **`finally`**: Always runs, typically for cleanup.
- **`raise`**: Used to manually raise exceptions.
- **Custom Exceptions**: You can define custom exceptions by subclassing `Exception`.

Lists in Python

A list in Python is an ordered, mutable collection of items. Lists can store elements of any data type (integers, strings, other lists, etc.), and they allow for indexing, slicing, and various built-in methods for modification.

1. Creating Lists

You can create a list by enclosing items in square brackets [].

Example:

my_list = [1, 2, 3, "apple", [5, 6]]
print(my_list)  # Output: [1, 2, 3, 'apple', [5, 6]]

2. Accessing List Elements

Elements in a list can be accessed using their index. The index starts at 0 for the first element.

Example:

my_list = ["apple", "banana", "cherry"]
print(my_list[0])  # Output: apple
print(my_list[-1])  # Output: cherry (negative indexing accesses elements from the end)

3. Slicing Lists

You can access a subset of list elements by using slicing.

Example:

my_list = [1, 2, 3, 4, 5]
print(my_list[1:4])  # Output: [2, 3, 4]
print(my_list[:3])   # Output: [1, 2, 3]
print(my_list[::2])  # Output: [1, 3, 5] (access every second element)

4. Modifying Lists

Since lists are mutable, you can modify their contents by accessing elements directly, adding, removing, or changing elements.

Modifying Individual Elements:

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

Adding Elements:

  • append(): Adds an element to the end of the list.
  • insert(): Inserts an element at a specified position.
  • extend(): Extends the list by adding all elements from another iterable.
my_list = [1, 2, 3]
my_list.append(4)
my_list.insert(1, "banana")
my_list.extend([5, 6])
print(my_list)  # Output: [1, 'banana', 2, 3, 4, 5, 6]

Removing Elements:

  • pop(): Removes and returns the element at the given index (or the last element by default).
  • remove(): Removes the first occurrence of a specific value.
  • clear(): Removes all elements from the list.
my_list = [1, 2, 3, 4]
my_list.pop(2)         # Removes and returns the element at index 2
my_list.remove(1)      # Removes the first occurrence of the value 1
my_list.clear()        # Empties the entire list
print(my_list)         # Output: []

5. List Methods

Python provides a variety of built-in methods for working with lists.

Common List Methods:

  • append(): Adds an element to the end of the list.
  • extend(): Extends the list with elements from another iterable.
  • insert(): Inserts an element at the specified position.
  • remove(): Removes the first occurrence of the element.
  • pop(): Removes and returns the element at the given index.
  • clear(): Removes all elements from the list.
  • index(): Returns the index of the first matching element.
  • count(): Returns the number of occurrences of a specified element.
  • sort(): Sorts the list in ascending order.
  • reverse(): Reverses the elements of the list.
  • copy(): Returns a shallow copy of the list.

Example of Sorting and Reversing:

my_list = [3, 1, 4, 1, 5, 9]
my_list.sort()  # Sorts the list in place
print(my_list)  # Output: [1, 1, 3, 4, 5, 9]

my_list.reverse()  # Reverses the list in place
print(my_list)     # Output: [9, 5, 4, 3, 1, 1]

6. List Comprehensions

List comprehensions provide a concise way to create lists. They are often used for creating lists based on existing lists, with optional filtering and transformations.

Example:

# Create a list of squares
squares = [x ** 2 for x in range(6)]
print(squares)  # Output: [0, 1, 4, 9, 16, 25]

# Create a list with a condition
even_squares = [x ** 2 for x in range(6) if x % 2 == 0]
print(even_squares)  # Output: [0, 4, 16]

7. Nested Lists

Lists can contain other lists, creating nested lists. You can access nested elements using multiple indices.

Example:

nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(nested_list[1][1])  # Output: 5

8. List Iteration

You can iterate over a list using a for loop.

Example:

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

9. Copying Lists

To make a copy of a list, you can use the copy() method or slicing.

Example:

my_list = [1, 2, 3]
new_list = my_list.copy()  # Shallow copy
new_list2 = my_list[:]     # Another way to copy

print(new_list)  # Output: [1, 2, 3]

10. Checking Membership

Use the in keyword to check if an item is in the list.

Example:

fruits = ["apple", "banana", "cherry"]
print("apple" in fruits)  # Output: True
print("orange" in fruits) # Output: False

11. Advanced List Operations

List Concatenation:

You can concatenate two lists using the + operator.

list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined = list1 + list2
print(combined)  # Output: [1, 2, 3, 4, 5, 6]

Repetition:

You can repeat a list multiple times using the * operator.

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

Summary of Lists in Python:

  • Lists are mutable, ordered collections that can hold elements of different types.
  • You can add, modify, and remove elements using various built-in methods.
  • Lists support indexing, slicing, and iteration.
  • List comprehensions provide a concise way to create and filter lists.
  • Nested lists allow for multi-dimensional data structures.

Tuples in Python

A tuple in Python is an ordered, immutable collection of items. Once created, the elements of a tuple cannot be changed. Tuples are often used when you want to store a collection of items that should not be modified.

1. Creating Tuples

Tuples are created by placing values inside parentheses () and separating them with commas. A tuple can hold elements of different data types.

Example:

my_tuple = (1, 2, 3, "apple", [5, 6])
print(my_tuple)  # Output: (1, 2, 3, 'apple', [5, 6])

Note: Parentheses are optional when defining a tuple. For example, my_tuple = 1, 2, 3 also creates a tuple.

2. Accessing Tuple Elements

Elements in a tuple can be accessed using their index. The index starts at 0 for the first element.

Example:

my_tuple = ("apple", "banana", "cherry")
print(my_tuple[0])  # Output: apple
print(my_tuple[-1])  # Output: cherry (negative indexing accesses elements from the end)

3. Tuple Immutability

Tuples are immutable, meaning once a tuple is created, you cannot modify its elements. However, if a tuple contains a mutable element like a list, that element can still be modified.

Example:

my_tuple = (1, 2, 3)
# my_tuple[0] = 10  # Raises TypeError: 'tuple' object does not support item assignment

# Modifying a mutable element inside a tuple
my_tuple = (1, [2, 3])
my_tuple[1][0] = 99
print(my_tuple)  # Output: (1, [99, 3])

4. Tuple Slicing

You can access a subset of tuple elements using slicing.

Example:

my_tuple = (1, 2, 3, 4, 5)
print(my_tuple[1:4])  # Output: (2, 3, 4)
print(my_tuple[:3])   # Output: (1, 2, 3)
print(my_tuple[::2])  # Output: (1, 3, 5) (access every second element)

5. Tuple Methods

Since tuples are immutable, they have fewer built-in methods compared to lists. However, two commonly used methods are:

  • count(): Returns the number of times a specified value occurs in a tuple.
  • index(): Returns the index of the first occurrence of a specified value.

Example:

my_tuple = (1, 2, 3, 2, 2, 4)
print(my_tuple.count(2))  # Output: 3 (2 appears 3 times)
print(my_tuple.index(3))  # Output: 2 (the first occurrence of 3 is at index 2)

6. Tuple Packing and Unpacking

Packing is the process of creating a tuple by assigning multiple values to a single variable. Unpacking is the reverse process, where the values in a tuple are assigned to multiple variables.

Example:

# Packing
my_tuple = 1, 2, 3

# Unpacking
a, b, c = my_tuple
print(a)  # Output: 1
print(b)  # Output: 2
print(c)  # Output: 3

You can also use the * operator to unpack the remaining elements.

Example of Extended Unpacking:

my_tuple = (1, 2, 3, 4, 5)
a, b, *rest = my_tuple
print(a)    # Output: 1
print(b)    # Output: 2
print(rest) # Output: [3, 4, 5]

7. Nested Tuples

Tuples can contain other tuples, creating nested tuples. You can access nested elements using multiple indices.

Example:

nested_tuple = ((1, 2), (3, 4), (5, 6))
print(nested_tuple[1][1])  # Output: 4

8. Tuple Iteration

You can iterate over a tuple using a for loop.

Example:

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

9. Concatenating Tuples

You can concatenate tuples using the + operator.

Example:

tuple1 = (1, 2, 3)
tuple2 = (4, 5, 6)
combined_tuple = tuple1 + tuple2
print(combined_tuple)  # Output: (1, 2, 3, 4, 5, 6)

10. Repetition of Tuples

You can repeat the elements of a tuple multiple times using the * operator.

Example:

my_tuple = (1, 2)
print(my_tuple * 3)  # Output: (1, 2, 1, 2, 1, 2)

11. Checking Membership in Tuples

Use the in keyword to check if an item exists in a tuple.

Example:

fruits = ("apple", "banana", "cherry")
print("apple" in fruits)  # Output: True
print("orange" in fruits) # Output: False

12. Copying Tuples

Since tuples are immutable, you don't need to create deep copies like you would with lists. You can simply assign a tuple to a new variable.

Example:

tuple1 = (1, 2, 3)
tuple2 = tuple1  # Shallow copy
print(tuple2)    # Output: (1, 2, 3)

13. Tuple as Dictionary Keys

Tuples can be used as keys in dictionaries because they are immutable, unlike lists.

Example:

my_dict = {(1, 2): "point1", (3, 4): "point2"}
print(my_dict[(1, 2)])  # Output: point1

14. Tuple vs. List: When to Use Each

  • Use tuples when you want to store a collection of items that should not change, such as coordinates or database records.
  • Use lists when you need a mutable collection, and you may need to add, remove, or modify elements over time.

Summary of Tuples in Python:

  • Tuples are immutable, ordered collections of elements.
  • They support indexing, slicing, and can contain multiple data types, including other tuples.
  • You can use tuple packing and unpacking to assign or retrieve values easily.
  • Tuples can be used as keys in dictionaries because they are immutable.
  • While fewer methods are available than lists, tuples are useful for storing data that should not be changed.

Dictionaries in Python

A dictionary in Python is an unordered, mutable collection that stores data in key-value pairs. Each key in a dictionary must be unique and immutable (such as strings, numbers, or tuples), while the values can be of any data type.

1. Creating Dictionaries

Dictionaries are created using curly braces {} or the dict() constructor, with key-value pairs separated by colons.

Example:

my_dict = {"name": "John", "age": 30, "city": "New York"}
print(my_dict)  # Output: {'name': 'John', 'age': 30, 'city': 'New York'}

# Using the dict() constructor
my_dict2 = dict(name="Alice", age=25, city="London")
print(my_dict2)  # Output: {'name': 'Alice', 'age': 25, 'city': 'London'}

2. Accessing Dictionary Values

You can access the values in a dictionary by using their corresponding keys.

Example:

my_dict = {"name": "John", "age": 30, "city": "New York"}
print(my_dict["name"])  # Output: John
print(my_dict["age"])   # Output: 30

3. Adding and Modifying Key-Value Pairs

You can add or modify key-value pairs in a dictionary by assigning a value to a key.

Example:

my_dict = {"name": "John", "age": 30}
my_dict["age"] = 31        # Modifying an existing key
my_dict["city"] = "Boston" # Adding a new key-value pair
print(my_dict)             # Output: {'name': 'John', 'age': 31, 'city': 'Boston'}

4. Removing Key-Value Pairs

You can remove items from a dictionary using several methods:

  • pop(): Removes the item with the specified key and returns its value.
  • del: Deletes the item with the specified key.
  • popitem(): Removes and returns the last inserted key-value pair.
  • clear(): Removes all items from the dictionary.

Example:

my_dict = {"name": "John", "age": 30, "city": "New York"}

# Using pop()
age = my_dict.pop("age")
print(age)      # Output: 30
print(my_dict)  # Output: {'name': 'John', 'city': 'New York'}

# Using del
del my_dict["city"]
print(my_dict)  # Output: {'name': 'John'}

# Using clear
my_dict.clear()
print(my_dict)  # Output: {}

5. Dictionary Methods

Python provides several built-in methods to work with dictionaries.

Common Dictionary Methods:

  • keys(): Returns a view object of all the keys in the dictionary.
  • values(): Returns a view object of all the values in the dictionary.
  • items(): Returns a view object of key-value pairs as tuples.
  • get(): Returns the value of a key, or None if the key doesn't exist.
  • update(): Updates the dictionary with the elements from another dictionary or iterable.

Example:

my_dict = {"name": "John", "age": 30}

# Get all keys
print(my_dict.keys())  # Output: dict_keys(['name', 'age'])

# Get all values
print(my_dict.values())  # Output: dict_values(['John', 30])

# Get key-value pairs
print(my_dict.items())  # Output: dict_items([('name', 'John'), ('age', 30)])

# Using get() method
print(my_dict.get("name"))    # Output: John
print(my_dict.get("city"))    # Output: None (doesn't raise an error if key doesn't exist)

# Using update()
my_dict.update({"city": "New York", "age": 31})
print(my_dict)  # Output: {'name': 'John', 'age': 31, 'city': 'New York'}

6. Iterating Over Dictionaries

You can iterate through a dictionary to access its keys, values, or key-value pairs.

Example:

my_dict = {"name": "John", "age": 30, "city": "New York"}

# Iterating over keys
for key in my_dict:
    print(key, my_dict[key])

# Iterating over values
for value in my_dict.values():
    print(value)

# Iterating over key-value pairs
for key, value in my_dict.items():
    print(key, value)

7. Dictionary Comprehensions

Similar to list comprehensions, Python supports dictionary comprehensions, allowing you to create dictionaries in a concise manner.

Example:

# Create a dictionary of squares
squares = {x: x ** 2 for x in range(1, 6)}
print(squares)  # Output: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Create a dictionary with a condition
even_squares = {x: x ** 2 for x in range(1, 6) if x % 2 == 0}
print(even_squares)  # Output: {2: 4, 4: 16}

8. Nested Dictionaries

Dictionaries can contain other dictionaries, creating nested dictionaries. You can access elements in a nested dictionary using multiple keys.

Example:

nested_dict = {
    "person1": {"name": "John", "age": 30},
    "person2": {"name": "Alice", "age": 25}
}
print(nested_dict["person1"]["name"])  # Output: John

9. Copying Dictionaries

To create a copy of a dictionary, you can use the copy() method or the dict() constructor.

Example:

original = {"name": "John", "age": 30}
copy1 = original.copy()  # Shallow copy
copy2 = dict(original)   # Another way to copy
print(copy1)  # Output: {'name': 'John', 'age': 30}

10. Checking for Keys

You can use the in keyword to check if a key exists in a dictionary.

Example:

my_dict = {"name": "John", "age": 30}
print("name" in my_dict)  # Output: True
print("city" in my_dict)  # Output: False

11. Merging Dictionaries

You can merge two dictionaries using the update() method or the new |= operator (Python 3.9+).

Example:

dict1 = {"name": "John", "age": 30}
dict2 = {"city": "New York", "age": 31}

# Using update() to merge dict2 into dict1
dict1.update(dict2)
print(dict1)  # Output: {'name': 'John', 'age': 31, 'city': 'New York'}

# Using | operator (Python 3.9+)
dict1 | dict2
print(dict1)  # Output: {'name': 'John', 'age': 31, 'city': 'New York'}

12. Tuples as Dictionary Keys

Since tuples are immutable, they can be used as keys in dictionaries. This is useful for representing complex keys.

Example:

coordinates = {(0, 0): "origin", (1, 2): "point1", (3, 4): "point2"}
print(coordinates[(1, 2)])  # Output: point1

Summary of Dictionaries in Python:

  • Dictionaries store key-value pairs and are mutable, allowing you to modify, add, or remove elements.
  • Common methods include keys(), values(), items(), get(), and update().
  • You can iterate over dictionaries and use dictionary comprehensions for concise creation.
  • Nested dictionaries allow for complex data structures, and tuples can be used as keys because they are immutable.

Sets in Python

A set in Python is an unordered collection of unique, immutable elements. Sets are commonly used when you want to store multiple items without duplicates. Since sets are unordered, they do not support indexing or slicing like lists or tuples.

1. Creating Sets

You can create a set by placing elements inside curly braces {} or by using the set() constructor.

Example:

my_set = {1, 2, 3, 4}
print(my_set)  # Output: {1, 2, 3, 4}

# Using set() constructor
my_set2 = set([1, 2, 2, 3, 4])  # Duplicates are removed
print(my_set2)  # Output: {1, 2, 3, 4}

Note: To create an empty set, you must use set(), not {}, as {} creates an empty dictionary.

2. Adding and Removing Elements in Sets

Sets are mutable, meaning you can add or remove elements after creating them.

Adding Elements:

  • add(): Adds a single element to the set.
  • update(): Adds multiple elements (can accept lists, tuples, or other sets).
my_set = {1, 2, 3}
my_set.add(4)  # Adds 4 to the set
my_set.update([5, 6])  # Adds multiple elements
print(my_set)  # Output: {1, 2, 3, 4, 5, 6}

Removing Elements:

  • remove(): Removes a specific element (raises KeyError if the element is not found).
  • discard(): Removes a specific element (does not raise an error if the element is not found).
  • pop(): Removes and returns an arbitrary element from the set.
  • clear(): Removes all elements from the set.
my_set = {1, 2, 3, 4}
my_set.remove(3)  # Removes 3 from the set
my_set.discard(5)  # No error if 5 is not in the set
removed_item = my_set.pop()  # Removes an arbitrary element
my_set.clear()  # Empties the set
print(my_set)  # Output: set()

3. Set Operations

Python provides several set operations like union, intersection, difference, and symmetric difference, which are useful for combining or comparing sets.

Union (| or union()):

Returns a set that contains all the elements from both sets.

set1 = {1, 2, 3}
set2 = {3, 4, 5}
print(set1 | set2)  # Output: {1, 2, 3, 4, 5}
print(set1.union(set2))  # Output: {1, 2, 3, 4, 5}

Intersection (& or intersection()):

Returns a set that contains only the elements present in both sets.

set1 = {1, 2, 3}
set2 = {3, 4, 5}
print(set1 & set2)  # Output: {3}
print(set1.intersection(set2))  # Output: {3}

Difference (- or difference()):

Returns a set that contains elements that are in the first set but not in the second.

set1 = {1, 2, 3}
set2 = {3, 4, 5}
print(set1 - set2)  # Output: {1, 2}
print(set1.difference(set2))  # Output: {1, 2}

Symmetric Difference (^ or symmetric_difference()):

Returns a set that contains elements that are in either set, but not in both.

set1 = {1, 2, 3}
set2 = {3, 4, 5}
print(set1 ^ set2)  # Output: {1, 2, 4, 5}
print(set1.symmetric_difference(set2))  # Output: {1, 2, 4, 5}

4. Set Comparisons

You can compare sets using comparison operators like <=, >=, and ==.

Example:

set1 = {1, 2, 3}
set2 = {1, 2, 3, 4}
print(set1 <= set2)  # Output: True (set1 is a subset of set2)
print(set2 >= set1)  # Output: True (set2 is a superset of set1)
print(set1 == set2)  # Output: False (sets are not equal)

5. Set Methods

Some useful set methods include:

  • add(): Adds a single element to the set.
  • update(): Adds multiple elements to the set.
  • remove(): Removes a specific element from the set.
  • discard(): Removes a specific element without raising an error.
  • clear(): Removes all elements from the set.
  • pop(): Removes and returns an arbitrary element from the set.
  • union(): Returns the union of two sets.
  • intersection(): Returns the intersection of two sets.
  • difference(): Returns the difference of two sets.
  • symmetric_difference(): Returns the symmetric difference of two sets.

6. Frozen Sets

A frozenset is an immutable version of a set. Once a frozenset is created, its elements cannot be changed, added, or removed.

Example:

my_set = frozenset([1, 2, 3, 4])
print(my_set)  # Output: frozenset({1, 2, 3, 4})

# my_set.add(5)  # Raises AttributeError: 'frozenset' object has no attribute 'add'

Frozen sets are useful when you need to use sets as keys in dictionaries, or in other contexts where immutability is required.

7. Iterating Over a Set

You can iterate over the elements of a set using a for loop.

Example:

my_set = {1, 2, 3, 4, 5}
for item in my_set:
    print(item)

8. Checking Membership in Sets

Use the in keyword to check if an element exists in a set.

Example:

my_set = {1, 2, 3, 4}
print(2 in my_set)  # Output: True
print(5 in my_set)  # Output: False

9. Set Comprehensions

Similar to list comprehensions, you can create sets using set comprehensions.

Example:

# Create a set of squares
squares = {x ** 2 for x in range(1, 6)}
print(squares)  # Output: {1, 4, 9, 16, 25}

# Create a set with a condition
even_squares = {x ** 2 for x in range(1, 6) if x % 2 == 0}
print(even_squares)  # Output: {4, 16}

10. Converting Between Lists, Tuples, and Sets

You can easily convert between lists, tuples, and sets using the list(), tuple(), and set() constructors.

Example:

# Convert list to set
my_list = [1, 2, 3, 4]
my_set = set(my_list)
print(my_set)  # Output: {1, 2, 3, 4}

# Convert set to list
my_set = {1, 2, 3, 4}
my_list = list(my_set)
print(my_list)  # Output: [1, 2, 3, 4]

11. Performance Considerations

Sets are optimized for membership checks, making operations like checking if an item exists (in keyword) faster than in lists or tuples. This makes sets ideal for tasks where you need to eliminate duplicates or perform frequent membership checks.

Summary of Sets in Python:

  • Sets are unordered collections of unique elements, optimized for fast membership checks.
  • You can perform mathematical operations like union, intersection, difference, and symmetric difference on sets.
  • Frozen sets are immutable versions of sets and can be used in situations where sets need to be immutable, like as dictionary keys.
  • Set comprehensions provide a concise way to create sets from iterables.

Functions in Python

A function in Python is a block of reusable code that performs a specific task. Functions help in organizing code, improving readability, and enabling reusability. Functions are defined using the def keyword.

1. Defining Functions

To define a function, use the def keyword followed by the function name and parentheses () that may contain parameters.

Syntax:

def function_name(parameters):
    # Code block
    return result

Example:

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

2. Calling Functions

Once defined, a function can be called by using its name followed by parentheses ().

Example:

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

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

3. Function Arguments

Functions can take arguments to make them more flexible. There are several types of function arguments in Python:

3.1 Positional Arguments

Positional arguments are passed to functions in the order in which they are defined.

def multiply(a, b):
    return a * b

result = multiply(4, 5)
print(result)  # Output: 20

3.2 Keyword Arguments

Keyword arguments allow you to pass arguments by specifying their names.

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

greet(name="Alice", message="Good morning")  # Output: Good morning, Alice!

3.3 Default Arguments

You can provide default values for arguments. If the argument is not provided, the default value is used.

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

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

3.4 Variable-Length Arguments

Python allows you to define functions with a variable number of arguments using *args for positional arguments and **kwargs for keyword arguments.

  • *args: Allows the function to accept any number of positional arguments.
  • **kwargs: Allows the function to accept any number of keyword arguments.
# Using *args for positional arguments
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3))  # Output: 6

# Using **kwargs for keyword arguments
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=25)  # Output: name: Alice, age: 25

4. Return Values

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

Example:

def square(number):
    return number * number

result = square(4)
print(result)  # Output: 16

5. Lambda Functions (Anonymous Functions)

Lambda functions are small, anonymous functions that can take any number of arguments but can only have one expression. They are often used for short, simple functions.

Syntax:

lambda arguments: expression

Example:

# A lambda function to add two numbers
add = lambda x, y: x + y
print(add(5, 3))  # Output: 8

# Using lambda with map() function
numbers = [1, 2, 3, 4]
squares = list(map(lambda x: x ** 2, numbers))
print(squares)  # Output: [1, 4, 9, 16]

6. Docstrings

A docstring is a string literal that appears right after the function definition. It is used to document the function. You can access the docstring using the __doc__ attribute.

Example:

def multiply(a, b):
    """This function multiplies two numbers."""
    return a * b

print(multiply.__doc__)  # Output: This function multiplies two numbers.

7. Nested Functions

You can define functions inside other functions. These are called nested functions or inner functions.

Example:

def outer_function(text):
    def inner_function():
        print(text)
    inner_function()

outer_function("Hello from the outer function!")  # Output: Hello from the outer function!

8. Closures

A closure is a function object that remembers values in enclosing scopes, even if those scopes are no longer present. Closures are created when a nested function references a value from its outer function.

Example:

def outer_function(text):
    def inner_function():
        print(text)
    return inner_function

my_func = outer_function("Hello!")
my_func()  # Output: Hello!

9. Decorators

A decorator is a function that takes another function as an argument and extends or modifies its behavior without explicitly modifying the function itself. Decorators are often used for logging, enforcing access control, or measuring execution time.

Example:

def decorator_function(original_function):
    def wrapper_function():
        print("Wrapper executed this before {}".format(original_function.__name__))
        original_function()
    return wrapper_function

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

say_hello()

Output:

Wrapper executed this before say_hello
Hello!

10. Recursion

A function can call itself, known as recursion. It is commonly used for problems that can be broken down into smaller, similar problems (like factorial or Fibonacci calculations).

Example:

def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n - 1)

print(factorial(5))  # Output: 120

11. Higher-Order Functions

A higher-order function is a function that takes another function as an argument or returns a function as a result. Common higher-order functions include map(), filter(), and reduce().

Example:

# Using map() as a higher-order function
numbers = [1, 2, 3, 4]
squares = list(map(lambda x: x ** 2, numbers))
print(squares)  # Output: [1, 4, 9, 16]

# Using filter() as a higher-order function
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # Output: [2, 4]

12. *args and **kwargs in Detail

Example:

  • *args allows a function to take a variable number of positional arguments:
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3))  # Output: 6
  • **kwargs allows a function to take a variable number of keyword arguments:
def print_details(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_details(name="Alice", age=25)  # Output: name: Alice, age: 25

Summary of Functions in Python:

  • Functions allow code reuse and help in organizing your programs.
  • Functions can take positional arguments, keyword arguments, default arguments, and variable-length arguments.
  • Functions can return values using the return statement, and can also have docstrings for documentation.
  • Lambda functions are anonymous, one-line functions, and decorators extend or modify the behavior of existing functions.
  • Python supports recursion and higher-order functions like map(), filter(), and reduce().

Modules in Python

A module in Python is a file containing Python code (functions, classes, variables) that can be imported and used in other Python programs. Modules help in organizing code into reusable blocks, improving the modularity and maintainability of large projects.

1. Creating a Module

A Python module is simply a .py file containing Python code. You can create your own module by writing functions, classes, or variables in a Python file.

Example:

Create a file called mymodule.py:

# mymodule.py

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

PI = 3.14159

Now, this mymodule.py can be imported into another Python script.

2. Importing Modules

To use a module in your program, you can import it using the import statement. You can import an entire module, specific functions, or use an alias.

Example: Importing the Entire Module

# main.py
import mymodule

print(mymodule.greet("Alice"))  # Output: Hello, Alice!
print(mymodule.PI)              # Output: 3.14159

Example: Importing Specific Functions or Variables

You can import only specific parts of a module using the from keyword.

# main.py
from mymodule import greet, PI

print(greet("Bob"))  # Output: Hello, Bob!
print(PI)            # Output: 3.14159

Example: Using Aliases for Modules

You can use the as keyword to give a module or function an alias for easier access.

# main.py
import mymodule as mm

print(mm.greet("Charlie"))  # Output: Hello, Charlie!
print(mm.PI)                # Output: 3.14159

3. Built-in Modules

Python comes with a large collection of built-in modules, like math, os, random, sys, and more. You can use these modules by importing them in your code.

Example: Using the math Module

import math

print(math.sqrt(16))  # Output: 4.0
print(math.pi)        # Output: 3.141592653589793

Example: Using the random Module

import random

print(random.randint(1, 10))  # Output: A random integer between 1 and 10

4. The dir() Function

The dir() function can be used to list all the names (functions, variables, etc.) defined in a module. This is useful for exploring the contents of both built-in and custom modules.

Example:

import math
print(dir(math))
# Output: List of all functions and variables in the math module

5. The __name__ Variable

Every Python module has a special built-in variable called __name__. When a module is run directly, __name__ is set to "__main__", but when it is imported, __name__ is set to the module's name. This is commonly used to control the execution of code only when a module is run directly.

Example:

In mymodule.py:

# mymodule.py

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

if __name__ == "__main__":
    print("This is executed when the module is run directly.")

If you run mymodule.py directly, the print statement will execute. If you import mymodule in another file, the print statement won't execute.

$ python mymodule.py
This is executed when the module is run directly.

6. Packages

A package is a collection of modules grouped in a directory. It allows for better organization of large projects. A package contains an __init__.py file, which can be empty or used to initialize the package.

Example: Package Structure

mypackage/ __init__.py module1.py module2.py

In module1.py:

def func1():
    print("Function 1 from module 1")

In module2.py:

def func2():
    print("Function 2 from module 2")

Now you can import the modules from the package.

Example:

from mypackage import module1, module2

module1.func1()  # Output: Function 1 from module 1
module2.func2()  # Output: Function 2 from module 2

7. Importing from a Package

You can use the from keyword to import specific functions or classes from a module inside a package.

Example:

from mypackage.module1 import func1
func1()  # Output: Function 1 from module 1

8. Relative Imports

Within a package, you can use relative imports to import modules relative to the current module's location.

Example:

# Inside mypackage/module2.py
from .module1 import func1

func1()  # Calls func1 from module1

9. Installing External Modules with pip

Python has a vast ecosystem of third-party modules available for installation via pip, the package installer for Python. You can install external modules from the Python Package Index (PyPI) using the command:

pip install <package_name>

Example:

pip install requests

After installing, you can import and use the requests module:

import requests

response = requests.get("https://www.example.com")
print(response.status_code)  # Output: 200

10. Re-importing Modules

If you modify a module after importing it, Python will not automatically re-import it unless you restart the interpreter. To re-import a module without restarting, you can use importlib.reload().

Example:

import mymodule
import importlib

# Modify mymodule.py here
importlib.reload(mymodule)  # Reloads the updated module

Python includes a large collection of standard libraries, such as:

  • os: For interacting with the operating system.
  • sys: For interacting with the Python runtime environment.
  • datetime: For working with dates and times.
  • json: For working with JSON data.
  • re: For regular expressions.

Example: Using the os Module

import os

print(os.getcwd())  # Output: Current working directory
os.mkdir("new_folder")  # Creates a new directory

Summary of Modules in Python:

  • Modules allow code to be organized into reusable blocks. A module is just a Python file, and it can contain functions, classes, or variables.
  • You can import entire modules or specific components using import and from ... import.
  • Packages are directories containing multiple modules, and they allow for better project organization.
  • Use relative imports within packages to reference sibling or parent modules.
  • Python has a vast collection of built-in modules, and third-party modules can be installed using pip.
  • The __name__ variable allows you to control whether code runs when a module is imported or executed directly.

Object-Oriented Programming in Python

Python supports object-oriented programming (OOP), a paradigm where everything is represented as objects. OOP focuses on organizing code around objects, which contain both data (attributes) and behavior (methods). Key concepts of OOP include classes, objects, inheritance, encapsulation, polymorphism, and abstraction.

1. Classes and Objects

A class is a blueprint for creating objects. An object is an instance of a class. Classes define properties and behaviors (attributes and methods) that the objects will have.

Example: Defining a Class

class Dog:
    def __init__(self, name, breed):
        self.name = name  # Attribute
        self.breed = breed  # Attribute
    
    def bark(self):
        print(f"{self.name} is barking!")

# Creating an object of the Dog class
my_dog = Dog("Buddy", "Golden Retriever")
print(my_dog.name)  # Output: Buddy
my_dog.bark()       # Output: Buddy is barking!
  • __init__() is a special method (constructor) used to initialize the attributes of a class.
  • self refers to the current instance of the class and is required in method definitions.

2. Attributes and Methods

  • Attributes are variables that belong to an object or class. They represent the state or properties of an object.
  • Methods are functions that belong to an object or class. They define the behavior of the object.

Example:

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

    def start_engine(self):
        print(f"The engine of {self.make} {self.model} has started.")
    
# Creating an object
my_car = Car("Toyota", "Corolla")
my_car.start_engine()  # Output: The engine of Toyota Corolla has started.

3. Encapsulation

Encapsulation is the principle of hiding the internal details of an object and providing controlled access through methods. In Python, this is achieved using private attributes (denoted by a leading underscore _ or double underscore __).

Example:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance  # Private attribute
    
    def deposit(self, amount):
        self.__balance += amount
    
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")

    def get_balance(self):
        return self.__balance

# Creating an object
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500
account.__balance = 0         # This won't change the balance, as it's private
print(account.get_balance())  # Output: 1500

4. Inheritance

Inheritance allows one class (child class) to inherit attributes and methods from another class (parent class). This promotes code reuse and hierarchical class relationships.

Example:

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

    def speak(self):
        print(f"{self.name} makes a sound.")

class Dog(Animal):  # Dog inherits from Animal
    def speak(self):
        print(f"{self.name} barks.")

class Cat(Animal):  # Cat inherits from Animal
    def speak(self):
        print(f"{self.name} meows.")

# Creating objects
dog = Dog("Buddy")
cat = Cat("Whiskers")

dog.speak()  # Output: Buddy barks.
cat.speak()  # Output: Whiskers meows.

5. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common parent class. It also enables method overriding, where child classes can modify methods inherited from the parent class.

Example:

class Shape:
    def area(self):
        pass

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

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

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.1416 * self.radius ** 2

# Using polymorphism
shapes = [Rectangle(3, 4), Circle(5)]

for shape in shapes:
    print(shape.area())

Output:

12
78.54

6. Abstraction

Abstraction is the concept of hiding complex implementation details and exposing only the necessary parts. In Python, abstraction is often implemented using abstract base classes (ABCs).

Example Using abc Module:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

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

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

# Cannot instantiate abstract class directly
# shape = Shape()  # Raises TypeError

# Can instantiate child class
rect = Rectangle(5, 6)
print(rect.area())  # Output: 30

7. Class and Static Methods

  • Class methods are methods that are bound to the class and not the instance. They are defined using the @classmethod decorator and take cls as their first parameter instead of self.
  • Static methods are methods that do not depend on the class or instance and are defined using the @staticmethod decorator.

Example:

class MathOperations:
    @staticmethod
    def add(a, b):
        return a + b
    
    @classmethod
    def description(cls):
        return f"This class contains basic math operations for the {cls.__name__}."

# Using static method
print(MathOperations.add(5, 3))  # Output: 8

# Using class method
print(MathOperations.description())  # Output: This class contains basic math operations for the MathOperations.

8. Multiple Inheritance

Python supports multiple inheritance, where a class can inherit from more than one parent class. This allows for more flexible code reuse but can also introduce complexity, such as the diamond problem, which Python resolves using the Method Resolution Order (MRO).

Example:

class A:
    def action(self):
        print("Action from class A")

class B:
    def action(self):
        print("Action from class B")

class C(A, B):  # Inherits from both A and B
    pass

obj = C()
obj.action()  # Output: Action from class A (due to MRO)

9. Dunder (Magic) Methods

Dunder (Double Underscore) Methods are special methods in Python that allow you to define how objects behave with built-in operations, like +, -, comparison operators, and string representations.

Example:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
    
    def __str__(self):
        return f"'{self.title}' by {self.author}"
    
    def __add__(self, other):
        return f"{self.title} and {other.title}"

book1 = Book("1984", "George Orwell")
book2 = Book("Brave New World", "Aldous Huxley")

print(book1)            # Output: '1984' by George Orwell
print(book1 + book2)    # Output: 1984 and Brave New World

10. Object Comparison and Hashing

You can control object comparison by implementing methods like __eq__(), __lt__(), etc. You can also control how objects are used in hash-based collections like dictionaries by implementing __hash__().

Example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.age == other.age

    def __hash__(self):
        return hash((self.name, self.age))

person1 = Person("Alice", 30)
person2 = Person("Bob", 30)

# Object comparison
print(person1 == person2)  # Output: True (compares age)

# Using objects as dictionary keys
people_dict = {person1: "Engineer", person2: "Doctor"}
print(people_dict[person1])  # Output: Engineer

Summary of Object-Oriented Programming in Python:

  • Classes are blueprints for creating objects, and objects are instances of classes.
  • Encapsulation hides internal details of an object, exposing only what is necessary through methods.
  • Inheritance allows one class to inherit attributes and methods from another, promoting code reuse.
  • Polymorphism enables using a single interface

File I/O in Python

File input/output (I/O) in Python allows you to work with files—reading from and writing to them. Python provides built-in functions and methods for interacting with files, such as open(), read(), write(), and others.

1. Opening Files

The open() function is used to open a file in Python. It takes two main arguments:

  • File path: The path to the file you want to open.
  • Mode: The mode in which to open the file (e.g., reading, writing, appending).

Syntax:

file = open("filename", "mode")

Common File Modes:

  • 'r': Read (default mode) – Opens a file for reading.
  • 'w': Write – Opens a file for writing (overwrites if the file exists, creates a new file if it doesn’t).
  • 'a': Append – Opens a file for appending (adds data to the end of the file if it exists).
  • 'x': Create – Creates a new file, raises an error if the file already exists.
  • 'b': Binary mode – Used for reading/writing binary files (e.g., images, executables).
  • 't': Text mode (default) – Used for reading/writing text files.

Example:

# Open a file for reading
file = open("example.txt", "r")
# Open a file for writing
file = open("output.txt", "w")
# Open a binary file for reading
file = open("image.jpg", "rb")

2. Reading from Files

You can read the contents of a file using methods like read(), readline(), and readlines().

Reading the Entire File:

  • read() reads the entire content of the file as a string.
file = open("example.txt", "r")
content = file.read()
print(content)
file.close()  # Always close the file after use

Reading Line by Line:

  • readline() reads one line at a time.
  • readlines() reads all lines and returns them as a list.
file = open("example.txt", "r")

# Reading one line
line = file.readline()
print(line)

# Reading all lines into a list
lines = file.readlines()
print(lines)

file.close()

3. Writing to Files

To write data to a file, use the write() or writelines() method. If the file is opened in write mode ('w'), the existing contents are overwritten. If opened in append mode ('a'), the new data is appended to the file.

Example:

# Writing a single string to a file
file = open("output.txt", "w")
file.write("Hello, World!")
file.close()

# Writing multiple lines to a file
lines = ["First line\n", "Second line\n", "Third line\n"]
file = open("output.txt", "w")
file.writelines(lines)
file.close()

4. Using with for File Handling

It is good practice to use the with statement when working with files. It automatically closes the file once the block is exited, even if exceptions are raised. This reduces the chances of leaving a file open unintentionally.

Example:

with open("example.txt", "r") as file:
    content = file.read()
    print(content)
# No need to explicitly close the file

5. Appending to Files

If you want to add data to the end of an existing file without overwriting it, open the file in append mode ('a').

Example:

with open("output.txt", "a") as file:
    file.write("This is appended text.\n")

6. Binary File I/O

Binary mode ('b') is used for reading and writing binary files (such as images, audio files, and executables). The data is read or written in the form of bytes, not strings.

Example: Reading a Binary File

with open("image.jpg", "rb") as file:
    binary_data = file.read()
    print(binary_data)

Example: Writing a Binary File

with open("output.bin", "wb") as file:
    file.write(b"\x00\xFF\x00\xFF")  # Writing raw bytes

7. File Positions and seek()

When reading or writing to a file, the file pointer moves. You can move the pointer to a specific position using seek() and find out the current position using tell().

Example:

file = open("example.txt", "r")
print(file.tell())  # Output: 0 (Initial position)

content = file.read(5)  # Read the first 5 characters
print(content)

file.seek(0)  # Move the file pointer back to the beginning
print(file.read(5))  # Read the first 5 characters again

file.close()

8. File Handling Exceptions

When working with files, exceptions such as FileNotFoundError or PermissionError may occur. It's important to handle these exceptions to prevent crashes.

Example:

try:
    with open("nonexistent.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("The file was not found.")
except PermissionError:
    print("Permission denied.")

9. Working with Directories using os and shutil

The os and shutil modules provide functions for interacting with the file system, such as creating directories, renaming, and deleting files.

Example: Checking if a File Exists

import os

if os.path.exists("example.txt"):
    print("The file exists.")
else:
    print("The file does not exist.")

Example: Renaming and Deleting Files

import os

# Renaming a file
os.rename("output.txt", "renamed_output.txt")

# Deleting a file
os.remove("renamed_output.txt")

Example: Working with Directories

import os

# Creating a directory
os.mkdir("new_directory")

# Changing the current working directory
os.chdir("new_directory")

# Listing files in the current directory
print(os.listdir("."))

# Removing a directory
os.rmdir("new_directory")

10. Copying and Moving Files using shutil

The shutil module provides higher-level file operations, such as copying and moving files.

Example:

import shutil

# Copying a file
shutil.copy("example.txt", "copy_of_example.txt")

# Moving a file
shutil.move("copy_of_example.txt", "moved_example.txt")

Summary of File I/O in Python:

  • File Modes: Use 'r' for reading, 'w' for writing, 'a' for appending, and 'b' for binary mode.
  • Reading: Use read(), readline(), or readlines() to read content from a file.
  • Writing: Use write() or writelines() to write content to a file.
  • with Statement: Automatically handles file closing after the block of code is executed.
  • Binary Files: Use 'b' mode for working with binary files.
  • Error Handling: Use try-except blocks to handle file-related exceptions like FileNotFoundError.
  • File Operations: Use the os and shutil modules for file system operations like renaming, copying, and deleting files.

Making HTTP Calls in Python

Python provides several libraries for making HTTP requests. One of the most popular and easy-to-use libraries is requests. It simplifies the process of making HTTP calls, handling response data, and managing headers and authentication.

1. Installing the requests Library

To get started with HTTP requests, you need to install the requests library if you haven't already. You can do this using pip.

pip install requests

2. Making a GET Request

The GET method is used to retrieve information from a server. The requests.get() method sends a GET request to the specified URL and returns a Response object.

Example:

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts")
print(response.status_code)  # Output: 200
print(response.text)  # Response content in plain text
print(response.json())  # Parse response content as JSON
  • response.status_code: The HTTP status code (e.g., 200 for success, 404 for not found).
  • response.text: The response content as a string.
  • response.json(): Parse the response as JSON (if the response content is in JSON format).

3. Query Parameters in GET Requests

You can pass query parameters in a GET request using the params argument.

Example:

url = "https://jsonplaceholder.typicode.com/posts"
params = {"userId": 1}

response = requests.get(url, params=params)
print(response.url)  # Output: The complete URL with query parameters
print(response.json())  # JSON response filtered by query parameters

4. Making a POST Request

The POST method is used to send data to a server, often for creating or updating resources. You can send data in the form of JSON, form-encoded data, or files.

Example: Sending JSON Data

url = "https://jsonplaceholder.typicode.com/posts"
data = {
    "title": "foo",
    "body": "bar",
    "userId": 1
}

response = requests.post(url, json=data)
print(response.status_code)  # Output: 201 (Created)
print(response.json())  # Output: JSON response from server
  • json=data: Sends the data as JSON in the body of the request.

5. Sending Form Data in a POST Request

You can send form data in a POST request using the data argument.

Example:

url = "https://httpbin.org/post"
data = {"name": "John", "age": 30}

response = requests.post(url, data=data)
print(response.status_code)  # Output: 200
print(response.text)  # Output: The form data received by the server

6. Sending Files in a POST Request

You can upload files using the files argument.

Example:

url = "https://httpbin.org/post"
files = {"file": open("example.txt", "rb")}

response = requests.post(url, files=files)
print(response.status_code)  # Output: 200
print(response.json())  # Output: Server response after file upload

7. Making PUT, DELETE, and PATCH Requests

Other HTTP methods, such as PUT, DELETE, and PATCH, can be used for updating or deleting resources.

Example: PUT Request (Update)

url = "https://jsonplaceholder.typicode.com/posts/1"
data = {
    "id": 1,
    "title": "foo updated",
    "body": "bar updated",
    "userId": 1
}

response = requests.put(url, json=data)
print(response.status_code)  # Output: 200 (OK)
print(response.json())  # Output: Updated resource

Example: DELETE Request

url = "https://jsonplaceholder.typicode.com/posts/1"

response = requests.delete(url)
print(response.status_code)  # Output: 200 (OK)

8. Handling Headers

You can customize HTTP headers in your request using the headers argument. This is useful for sending authentication tokens, content types, and more.

Example:

url = "https://jsonplaceholder.typicode.com/posts"
headers = {"Authorization": "Bearer your_token"}

response = requests.get(url, headers=headers)
print(response.status_code)  # Output: 200 (if authorized)

9. Handling Timeouts

To avoid waiting indefinitely for a response, you can set a timeout using the timeout parameter. The request will raise a Timeout exception if the server takes longer than the specified time to respond.

Example:

url = "https://jsonplaceholder.typicode.com/posts"

try:
    response = requests.get(url, timeout=5)  # Timeout after 5 seconds
    print(response.status_code)
except requests.Timeout:
    print("The request timed out")

10. Handling Errors and Exceptions

The requests library automatically raises exceptions for HTTP errors. You can handle these exceptions using a try-except block.

Common Exceptions:

  • requests.Timeout: Raised when a request times out.
  • requests.ConnectionError: Raised when there is a network problem.
  • requests.HTTPError: Raised for invalid HTTP responses (like 4xx or 5xx).

Example:

url = "https://jsonplaceholder.typicode.com/posts/invalid"

try:
    response = requests.get(url)
    response.raise_for_status()  # Raises HTTPError for bad responses
except requests.HTTPError as err:
    print(f"HTTP error occurred: {err}")
except requests.ConnectionError:
    print("Network connection error")
except requests.Timeout:
    print("Request timed out")
except requests.RequestException as err:
    print(f"An error occurred: {err}")

11. Session Objects

You can create a session object using requests.Session() to persist certain parameters (e.g., headers, cookies) across multiple requests. This can improve performance and help maintain state (like keeping a user logged in).

Example:

session = requests.Session()
session.headers.update({"Authorization": "Bearer your_token"})

# Subsequent requests will use the session
response = session.get("https://jsonplaceholder.typicode.com/posts")
print(response.status_code)
session.close()  # Always close the session when done

12. Working with Cookies

You can retrieve and send cookies using the cookies attribute in the requests library.

Example: Sending and Retrieving Cookies

url = "https://httpbin.org/cookies"

# Sending cookies
cookies = {"session_id": "123456"}
response = requests.get(url, cookies=cookies)
print(response.json())  # Output: Cookies sent to the server

# Retrieving cookies from the response
response_cookies = response.cookies
print(response_cookies)

13. Redirects

By default, the requests library follows HTTP redirects. You can control this behavior by passing allow_redirects=False.

Example:

url = "http://httpbin.org/redirect/1"

# Follow redirects
response = requests.get(url)
print(response.url)  # Output: Final URL after the redirect

# Disable redirect following
response = requests.get(url, allow_redirects=False)
print(response.status_code)  # Output: 302 (Redirect status)

14. SSL Verification

The requests library verifies SSL certificates by default. You can disable SSL verification using the verify=False parameter, though it's generally not recommended.

Example:

url = "https://example.com"

response = requests.get(url, verify=False)  # Disable SSL verification
print(response.status_code)

Note: Disabling SSL verification can make the connection insecure.

Summary of HTTP Requests in Python:

  • GET: Use requests.get() to retrieve data from a server.
  • POST: Use requests.post() to send data to a server (JSON, form data, files).
  • PUT, DELETE: Use requests.put() and requests.delete() to update or delete resources.
  • Headers and cookies: Customize requests by adding headers and cookies.
  • Error handling: Handle common exceptions like Timeout, HTTPError, and ConnectionError.
  • Sessions: Use requests.Session() for persistent settings across multiple requests.

Working with Databases in Python

Python provides several libraries for working with databases, including support for relational databases like SQLite, PostgreSQL, and MySQL. You can use the sqlite3 module for lightweight local databases, or psycopg2 for PostgreSQL and mysql-connector-python for MySQL.

1. Working with SQLite

SQLite is a lightweight, serverless database that is built into Python via the sqlite3 module. It is ideal for small to medium-sized applications or development environments.

1.1 Connecting to SQLite Database

You can connect to an SQLite database using sqlite3.connect(). If the database file does not exist, it will be created.

Example:

import sqlite3

# Connect to SQLite database (or create it if it doesn't exist)
conn = sqlite3.connect("example.db")
cursor = conn.cursor()

# Create a table
cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER NOT NULL
    )
""")

# Commit changes and close connection
conn.commit()
conn.close()

1.2 Inserting Data into SQLite

To insert data into a table, you use the INSERT INTO SQL command with the execute() method.

Example:

conn = sqlite3.connect("example.db")
cursor = conn.cursor()

# Insert data into the table
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30))
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Bob", 25))

# Commit changes and close connection
conn.commit()
conn.close()

1.3 Fetching Data from SQLite

You can fetch data from the database using the SELECT SQL command and the fetchall() or fetchone() methods.

Example:

conn = sqlite3.connect("example.db")
cursor = conn.cursor()

# Fetch all data from the users table
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()

for row in rows:
    print(row)

conn.close()

1.4 Updating and Deleting Data in SQLite

You can update or delete records using the UPDATE and DELETE SQL commands.

Example: Updating Data

conn = sqlite3.connect("example.db")
cursor = conn.cursor()

# Update user data
cursor.execute("UPDATE users SET age = ? WHERE name = ?", (32, "Alice"))

# Commit changes and close connection
conn.commit()
conn.close()

Example: Deleting Data

conn = sqlite3.connect("example.db")
cursor = conn.cursor()

# Delete user data
cursor.execute("DELETE FROM users WHERE name = ?", ("Bob",))

# Commit changes and close connection
conn.commit()
conn.close()

2. Working with PostgreSQL

For working with PostgreSQL, you can use the psycopg2 library, which is the most popular PostgreSQL adapter for Python.

2.1 Installing psycopg2

To install psycopg2, run:

pip install psycopg2

2.2 Connecting to PostgreSQL

You can connect to a PostgreSQL database using psycopg2.connect() by providing database credentials.

Example:

import psycopg2

# Connect to PostgreSQL database
conn = psycopg2.connect(
    dbname="example_db",
    user="your_username",
    password="your_password",
    host="localhost",
    port="5432"
)
cursor = conn.cursor()

# Create a table
cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100),
        position VARCHAR(100),
        salary INTEGER
    )
""")

# Commit changes and close connection
conn.commit()
conn.close()

2.3 Inserting Data into PostgreSQL

To insert data into a PostgreSQL table, use the INSERT INTO command with placeholders %s.

Example:

conn = psycopg2.connect(dbname="example_db", user="your_username", password="your_password")
cursor = conn.cursor()

# Insert data into the employees table
cursor.execute("INSERT INTO employees (name, position, salary) VALUES (%s, %s, %s)", ("John", "Manager", 60000))
cursor.execute("INSERT INTO employees (name, position, salary) VALUES (%s, %s, %s)", ("Alice", "Developer", 55000))

# Commit changes and close connection
conn.commit()
conn.close()

2.4 Fetching Data from PostgreSQL

You can use SELECT to fetch data and fetchall() or fetchone() to retrieve results.

Example:

conn = psycopg2.connect(dbname="example_db", user="your_username", password="your_password")
cursor = conn.cursor()

# Fetch all data from the employees table
cursor.execute("SELECT * FROM employees")
rows = cursor.fetchall()

for row in rows:
    print(row)

conn.close()

3. Working with MySQL

For working with MySQL databases in Python, you can use the mysql-connector-python library.

3.1 Installing MySQL Connector

To install the MySQL connector, run:

pip install mysql-connector-python

3.2 Connecting to MySQL

You can connect to a MySQL database using mysql.connector.connect() by providing the database credentials.

Example:

import mysql.connector

# Connect to MySQL database
conn = mysql.connector.connect(
    host="localhost",
    user="your_username",
    password="your_password",
    database="example_db"
)
cursor = conn.cursor()

# Create a table
cursor.execute("""
    CREATE TABLE IF NOT EXISTS customers (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(100),
        address VARCHAR(255)
    )
""")

# Commit changes and close connection
conn.commit()
conn.close()

3.3 Inserting Data into MySQL

You can insert data into MySQL using the INSERT INTO command with placeholders %s.

Example:

conn = mysql.connector.connect(host="localhost", user="your_username", password="your_password", database="example_db")
cursor = conn.cursor()

# Insert data into the customers table
cursor.execute("INSERT INTO customers (name, address) VALUES (%s, %s)", ("John Doe", "123 Main St"))
cursor.execute("INSERT INTO customers (name, address) VALUES (%s, %s)", ("Jane Smith", "456 Maple Ave"))

# Commit changes and close connection
conn.commit()
conn.close()

3.4 Fetching Data from MySQL

You can retrieve data from MySQL using the SELECT command and fetchall() or fetchone() methods.

Example:

conn = mysql.connector.connect(host="localhost", user="your_username", password="your_password", database="example_db")
cursor = conn.cursor()

# Fetch all data from the customers table
cursor.execute("SELECT * FROM customers")
rows = cursor.fetchall()

for row in rows:
    print(row)

conn.close()

4. Parameter Binding to Prevent SQL Injection

When inserting data into a database, you should use parameterized queries to prevent SQL injection attacks. Most database connectors support this using placeholders (? for SQLite, %s for PostgreSQL and MySQL).

Example for PostgreSQL:

conn = psycopg2.connect(dbname="example_db", user="your_username", password="your_password")
cursor = conn.cursor()

# Safe parameterized query
cursor.execute("INSERT INTO employees (name, position, salary) VALUES (%s, %s, %s)", ("Alice", "Developer", 50000))

conn.commit()
conn.close()

5. Transactions in Databases

Most databases support transactions, which allow you to group multiple SQL statements into a single, atomic operation. In Python, this is done by calling conn.commit() after executing the SQL statements.

Example:

conn = psycopg2.connect(dbname="example_db", user="your_username", password="your_password")
cursor = conn.cursor()

try:
    # Start a transaction
    cursor.execute("UPDATE employees SET salary = salary + 5000 WHERE name = %s", ("John",))
    cursor.execute("INSERT INTO employees (name, position, salary) VALUES (%s, %s, %s)", ("Diana", "Tester", 45000))
    
    # Commit the transaction
    conn.commit()
except:
    # Rollback the transaction in case of an error
    conn.rollback()

conn.close()

Summary of Database Operations in Python:

  • SQLite: Use the built-in sqlite3 module for lightweight, serverless databases.
  • PostgreSQL: Use the psycopg2 library for working with PostgreSQL databases.
  • MySQL: Use the mysql-connector-python library for working with MySQL databases.
  • CRUD operations: You can perform create, read, update, and **delete

Generators in Python

Generators in Python are special types of iterators that allow you to iterate through a sequence of values without storing the entire sequence in memory at once. This makes them memory efficient, especially when working with large datasets. A generator yields values one at a time using the yield keyword.

1. Creating Generators

Generators are created using functions and the yield keyword. When a generator function is called, it doesn’t execute its code immediately but returns a generator object. The function's code runs only when you iterate over the generator or explicitly request the next value using next().

Example: Basic Generator

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()  # Create generator object
print(next(gen))  # Output: 1
print(next(gen))  # Output: 2
print(next(gen))  # Output: 3
# Raises StopIteration when no more items are available

2. Generator Expressions

Generator expressions are similar to list comprehensions, but they return a generator object instead of a list. They use parentheses () instead of square brackets [].

Example:

# Generator expression to create a sequence of squares
gen = (x ** 2 for x in range(5))

for value in gen:
    print(value)
# Output: 0 1 4 9 16

3. Advantages of Generators

  • Memory Efficiency: Generators yield items one at a time, making them memory-efficient because they don’t require loading all values into memory at once.
  • Lazy Evaluation: Values are generated only when requested, making generators faster for larger datasets or infinite sequences.
  • Simplified Iteration: Generators provide a simple way to implement custom iterators without needing to write classes and maintain state.

4. Using yield in Generators

The yield statement pauses the function, saving all its state, and then resumes from where it left off when next() is called again. This allows generators to produce a sequence of values lazily.

Example: Using yield

def countdown(n):
    while n > 0:
        yield n
        n -= 1

gen = countdown(5)

print(next(gen))  # Output: 5
print(next(gen))  # Output: 4
print(next(gen))  # Output: 3
# Continue until the generator is exhausted

5. Infinite Generators

Generators can be used to produce infinite sequences. Be careful with infinite generators as they can run indefinitely unless properly handled.

Example: Infinite Generator

def infinite_count(start=0):
    while True:
        yield start
        start += 1

gen = infinite_count()

# Fetch first 5 values
for _ in range(5):
    print(next(gen))
# Output: 0 1 2 3 4

6. Generator Methods: send(), close(), and throw()

Generators also support methods like send(), close(), and throw() for more advanced control.

  • send(value): Sends a value to the generator, which can be received using the yield expression.
  • close(): Closes the generator, raising a GeneratorExit exception inside the generator function.
  • throw(type): Throws an exception inside the generator at the point of the yield.

Example: Using send()

def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

gen = accumulator()
print(next(gen))  # Initialize and output: 0
print(gen.send(10))  # Output: 10
print(gen.send(20))  # Output: 30
gen.close()

7. Chaining Generators

You can chain multiple generators together to create more complex data pipelines. This is useful when you want to process data in multiple steps without loading everything into memory.

Example:

def generator1():
    for i in range(3):
        yield i

def generator2():
    for value in generator1():
        yield value * 2

for item in generator2():
    print(item)
# Output: 0 2 4

8. Using Generators with Built-in Functions

Generators work seamlessly with built-in Python functions like sum(), max(), min(), sorted(), and others. These functions can consume a generator without needing the entire dataset in memory.

Example:

gen = (x for x in range(1000000) if x % 2 == 0)

# Use sum() with a generator
total = sum(gen)
print(total)  # Sum of even numbers from 0 to 999999

9. Comparing Generators with Iterators

Generators are a type of iterator, but they are defined differently. Here’s a quick comparison:

FeatureGenerator (Function)Iterator (Class)
SyntaxDefined with yieldRequires __iter__() and __next__() methods
State HandlingAutomatic (using yield)Manual (you need to maintain state)
Memory UsageLazy (one item at a time)Lazy (but you need to write custom logic)
ExampleFunction-basedClass-based

10. Performance and Use Cases for Generators

Generators are ideal when:

  • You are working with large datasets or streams of data that don’t fit into memory.
  • You want to create pipelines for data processing, where each step generates a new item to be processed by the next.
  • You need infinite sequences, like counting numbers or processing a continuous stream of data.

Generators are not ideal if you need to:

  • Access elements by index, as generators do not support random access.

Creating a Custom Iterator in Python

In Python, an iterator is an object that can be iterated over, meaning it returns data one element at a time. You can create custom iterators by defining a class that implements the __iter__() and __next__() methods.

  • __iter__(): Returns the iterator object itself.
  • __next__(): Returns the next value in the sequence. When there are no more items to return, it should raise a StopIteration exception.

1. Basic Custom Iterator Example

Let's create a custom iterator that returns numbers from 1 to a specified limit.

Example:

class CountToLimit:
    def __init__(self, limit):
        self.limit = limit
        self.current = 0

    def __iter__(self):
        return self  # The iterator object itself

    def __next__(self):
        self.current += 1
        if self.current <= self.limit:
            return self.current
        else:
            raise StopIteration  # No more elements to iterate

# Using the custom iterator
counter = CountToLimit(5)

for num in counter:
    print(num)

Output:

1
2
3
4
5

2. Understanding the Custom Iterator

  • __iter__(): This method returns the iterator object itself. It is called once when the iterator is initialized (e.g., when a for loop begins).
  • __next__(): This method is called every time the next item in the sequence is requested. When the sequence is exhausted, it raises the StopIteration exception to signal the end.

3. Re-iterable Custom Iterator

The above example doesn't allow the iterator to be reused after it's exhausted. If you want your iterator to be re-iterable, you can modify the __iter__() method to return a new instance of the iterator.

Example:

class CountToLimit:
    def __init__(self, limit):
        self.limit = limit

    def __iter__(self):
        return CountToLimitIterator(self.limit)  # Return a new iterator instance

class CountToLimitIterator:
    def __init__(self, limit):
        self.limit = limit
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.current += 1
        if self.current <= self.limit:
            return self.current
        else:
            raise StopIteration

# Using the re-iterable custom iterator
counter = CountToLimit(3)

for num in counter:
    print(num)  # Output: 1 2 3

# Re-using the iterator
for num in counter:
    print(num)  # Output: 1 2 3

4. Custom Iterable with a Generator

You can also create custom iterators using generators, which simplify the iterator creation process using the yield keyword.

Example:

class CountToLimit:
    def __init__(self, limit):
        self.limit = limit

    def __iter__(self):
        current = 0
        while current < self.limit:
            current += 1
            yield current

# Using the custom iterator with a generator
counter = CountToLimit(4)

for num in counter:
    print(num)

5. Advantages of Custom Iterators

  • Flexibility: You can define custom iteration logic to suit any need (e.g., iterating over custom data structures, streams, or APIs).
  • Efficiency: You can save memory by not loading all data into memory at once (especially for large datasets), just like using generators.

Summary of Custom Iterators in Python:

  • Custom iterators require two methods: __iter__() and __next__().
  • __iter__() returns the iterator object itself or a new instance, depending on whether you want the iterator to be re-iterable.
  • __next__() returns the next item or raises StopIteration when the iteration is complete.
  • You can also use generators with yield to simplify creating custom iterators.

Measuring Code Performance with timeit in Python

The timeit module in Python is used to measure the execution time of small code snippets. It helps in benchmarking the performance of code by running it multiple times and providing an average time. This is useful for comparing different implementations of the same task or optimizing code.

1. Basic Usage of timeit.timeit()

The timeit.timeit() function is the most commonly used method for timing small code snippets. By default, it runs the code 1,000,000 times (for small snippets) and returns the total time taken.

Syntax:

timeit.timeit(stmt='pass', setup='pass', timer=<default>, number=1000000, globals=None)
  • stmt: The code snippet to be timed (as a string).
  • setup: The setup code that runs once before the stmt (useful for imports or variable initialization).
  • number: The number of times the code is executed (default: 1,000,000).
  • globals: A dictionary to specify the global variables (optional).

Example: Measuring a Basic Code Snippet

import timeit

# Measuring time for a list comprehension
time_taken = timeit.timeit('[x**2 for x in range(1000)]', number=10000)
print(f"Time taken: {time_taken} seconds")

In this example, the list comprehension is executed 10,000 times, and timeit returns the total execution time.

2. Using timeit with a Setup Block

If you need to set up some variables or import modules, you can pass a setup block. This code is executed once before the timed code runs.

Example: Using a Setup Block

import timeit

# Measuring time for sorting a list with setup
time_taken = timeit.timeit('sorted(arr)', setup='arr = list(range(1000, 0, -1))', number=10000)
print(f"Time taken: {time_taken} seconds")

In this example, the setup code creates a reverse-ordered list arr before timing the sorting operation.

3. Using timeit with Functions

You can also pass function calls to timeit.timeit() using the globals() argument to access global functions or variables.

Example: Timing a Function Call

import timeit

def example_function():
    return [x**2 for x in range(1000)]

# Timing the function call
time_taken = timeit.timeit('example_function()', globals=globals(), number=10000)
print(f"Time taken: {time_taken} seconds")

Here, the globals() argument allows timeit to access the example_function() defined in the current namespace.

4. Using timeit.repeat() for Multiple Timings

If you want to run the timing experiment multiple times and get a list of results, you can use the timeit.repeat() method. This allows you to measure variations in execution time.

Example: Using timeit.repeat()

import timeit

# Measure the time for multiple runs
times = timeit.repeat('[x**2 for x in range(1000)]', number=10000, repeat=5)
print(f"Execution times: {times}")

This runs the code snippet 10,000 times in 5 separate rounds and returns a list of execution times for each round. This is useful for observing variance in the execution time.

5. Timing Code in Jupyter or IPython Notebooks

If you're working in a Jupyter notebook or an IPython environment, you can use the built-in %timeit magic command, which simplifies timing code snippets.

Example: Using %timeit in Jupyter

%timeit [x**2 for x in range(1000)]

This automatically runs the code multiple times and provides the average time and standard deviation.

6. Timing Code with Custom Iterations

You can control the number of iterations using the number parameter. For very fast code snippets, you may need to increase the number to get accurate timing results.

Example: Timing with a Custom Number of Iterations

import timeit

# Increase the number of executions for accuracy
time_taken = timeit.timeit('sum(range(100))', number=100000)
print(f"Time taken for sum(range(100)): {time_taken} seconds")

7. Comparing Different Implementations

You can use timeit to compare the performance of different implementations of the same task.

Example: Comparing List Comprehension vs. map()

import timeit

# List comprehension
list_comp_time = timeit.timeit('[x**2 for x in range(1000)]', number=10000)

# map() function
map_time = timeit.timeit('list(map(lambda x: x**2, range(1000)))', number=10000)

print(f"List comprehension time: {list_comp_time}")
print(f"map() function time: {map_time}")

8. Limitations of timeit

  • Overhead: Small code snippets may suffer from overhead due to repeated function calls. For extremely fast code, the timeit overhead may distort the results.
  • Garbage Collection: By default, timeit disables Python's garbage collector. This prevents interference with the results, but in some cases, you might want to manually control it.

Enabling Garbage Collection:

import timeit

time_taken = timeit.timeit('[x**2 for x in range(1000)]', number=10000, globals=globals(), gc=True)
print(f"Time taken with garbage collection: {time_taken} seconds")

Summary of timeit in Python:

  • timeit.timeit(): Measures the time taken to execute a code snippet.
  • timeit.repeat(): Repeats the timing experiment multiple times for more accurate results.
  • Setup and Globals: You can provide setup code and global variables using the setup and globals() arguments.
  • Use Case: timeit is commonly used for benchmarking and comparing different implementations.
  • IPython/Jupyter %timeit: A convenient way to time code snippets in notebooks.

Local and Global Scope, nonlocal, global, Namespaces, and import *

1. Local and Global Scope

In Python, scope defines where variables can be accessed. Python has local, global, and nonlocal scopes:

  • Local scope: Variables defined inside a function. These variables are only accessible within that function.
  • Global scope: Variables defined outside of all functions. They can be accessed anywhere in the module, including inside functions (unless shadowed by local variables).

Example: Local and Global Scope

x = 10  # Global variable

def my_function():
    x = 5  # Local variable
    print(f"Inside function: {x}")

my_function()
print(f"Outside function: {x}")

Output:

Inside function: 5
Outside function: 10

In this example, x inside the function is a local variable that doesn’t affect the global variable x.

2. Global Keyword

The global keyword allows you to modify a global variable inside a function. Without global, any assignment to a variable inside a function creates a local variable.

Example: Using global Keyword

x = 10  # Global variable

def modify_global():
    global x
    x = 5  # Modify the global variable
    print(f"Inside function: {x}")

modify_global()
print(f"Outside function: {x}")

Output:

Inside function: 5
Outside function: 5

By using global, the global variable x is modified inside the function.

3. Nonlocal Keyword

The nonlocal keyword allows you to modify a variable in the enclosing (non-global) scope. This is useful in nested functions when you want to modify a variable in the outer (but not global) function.

Example: Using nonlocal Keyword

def outer_function():
    x = 10

    def inner_function():
        nonlocal x
        x = 20
        print(f"Inside inner function: {x}")

    inner_function()
    print(f"Inside outer function: {x}")

outer_function()

Output:

Inside inner function: 20
Inside outer function: 20

Here, nonlocal allows the inner function to modify the x variable defined in the outer function.

4. Namespaces

A namespace is a mapping between names and objects. In Python, different types of namespaces exist:

  • Local namespace: Inside functions.
  • Global namespace: At the module level.
  • Built-in namespace: Contains Python’s built-in functions (like print(), len()).

You can use the locals() and globals() functions to view the local and global namespaces, respectively.

Example: Viewing Namespaces

x = 10  # Global variable

def my_function():
    y = 5  # Local variable
    print("Local namespace:", locals())

my_function()
print("Global namespace:", globals().keys())  # Show all global variables

5. Built-in Functions and Namespace

Python provides many built-in functions (like len(), range(), min(), max()). These are part of the built-in namespace, which is automatically available in every Python program.

Example:

# Using built-in functions
print(len([1, 2, 3]))  # Output: 3
print(max([1, 5, 3]))  # Output: 5

You can view all the built-in functions using dir(__builtins__).

Example:

print(dir(__builtins__))

6. import * and Importing Modules

Using import * imports all names (functions, variables, classes) from a module into the current namespace. However, it is not recommended due to namespace pollution, which can lead to conflicts between imported names and local variables.

Example:

from math import *

print(sqrt(16))  # Output: 4.0

Here, all functions from the math module are imported. However, it's better to use import math to avoid conflicts.

7. Turtle Method

The turtle module is used for graphics and drawing. It provides a way to control a virtual turtle that moves around the screen.

Example: Drawing a Square with Turtle

import turtle

# Set up the turtle screen
screen = turtle.Screen()
t = turtle.Turtle()

# Draw a square
for _ in range(4):
    t.forward(100)  # Move forward 100 units
    t.right(90)     # Turn right 90 degrees

# Close the turtle window when clicked
screen.exitonclick()

In this example, the turtle moves forward and turns right to draw a square.

8. Python's global, nonlocal, and Scope in a Nutshell

  • Local scope: Variables defined within a function.
  • Global scope: Variables defined outside of any function.
  • global keyword: Allows modifying global variables inside functions.
  • nonlocal keyword: Allows modifying variables in an enclosing (non-global) function.
  • Namespaces: Python has local, global, and built-in namespaces.
  • Built-in functions: Automatically available and include functions like len(), print(), and range().

Summary of Scope, Namespaces, and Keywords:

  • Scope: Determines the visibility of variables (local vs. global scope).
  • global keyword: Used to modify global variables inside functions.
  • nonlocal keyword: Used to modify variables in the enclosing function.
  • Namespaces: Python uses local, global, and built-in namespaces.
  • import *: Imports all names from a module but is discouraged due to potential name conflicts.
  • turtle: Provides tools for drawing and controlling a virtual turtle for graphics.