# Overview

🐍 Python guide for beginners.

<figure><img src="https://1979105360-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCnRQCsYT2vYhaXTxEvZe%2Fuploads%2FTXRwNFivKliHU65NgBtD%2FCover.svg?alt=media&amp;token=1bda2750-91cf-48b2-80a2-78575df08dd9" alt=""><figcaption></figcaption></figure>


# Python for starters

This book provides beginners with a comprehensive guide that helps them learn the basics of the Python programming language and build a solid foundation for further exploration and development.

<figure><img src="https://1979105360-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCnRQCsYT2vYhaXTxEvZe%2Fuploads%2Fa2ERoallB36X3tTxC1j4%2F5588532.png?alt=media&amp;token=a719ddcb-9c9b-4cb8-9c4e-1dc2d6d26973" alt=""><figcaption></figcaption></figure>

Written by [Marv](https://solomonmarvel.com) with contributions from the [Community](https://topuniverse.org/).

Going through the examples in this book, it is assumed you’re using a python version that is >3.4.&#x20;

See the “[Installation](/introduction-to-python/python-installation)” section to install or update the python interpreter.


# Content

{% hint style="success" %}

#### Introduction to python

In this section, you will get an introductory overview of Python, highlighting its key features, applications, and importance, aimed at giving readers a foundational understanding of the language.
{% endhint %}

✅ [Introduction to python](/introduction-to-python)

* [Installation](/introduction-to-python/python-installation)
* [Variables](/introduction-to-python/variables)
* [Python Data Types](/introduction-to-python/python-data-types)
* [Python Data Structures](/introduction-to-python/python-data-structures)
* [Python numbers](/introduction-to-python/python-numbers)
* [Python strings](/introduction-to-python/python-strings)
* [Python Boolean](/introduction-to-python/python-boolean)
* [Python constants](/introduction-to-python/python-constants)
* [Comments](/introduction-to-python/python-comments)
* [Type Conversion](/introduction-to-python/type-conversion)
* [Control flow in python](/introduction-to-python/control-flow-in-python)

{% hint style="success" %}

#### Working with functions

In this section, you will get a concise overview of Python functions, explaining their purpose, syntax, usage, and features, serving as a quick reference for understanding and utilising functions in Python programming.
{% endhint %}

✅ [Working with Functions](/python-functions)

* [Function Definition](/python-functions/function-definition)
* [Function Parameter](/python-functions/function-parameter)
* [Recursive Functions](/python-functions/recursive-functions)
* [Using Global Variables in Functions](/python-functions/using-global-variables-in-functions)
* [Lambda Functions](/python-functions/lambda-functions)

{% hint style="success" %}

#### Python Data Structures

In this section, you will get an overview of data structures, their importance in computer science, and specifically discuss the built-in data structures available in Python.
{% endhint %}

✅ [Python Data Structures](/what-are-data-structures)

* [Lists](/what-are-data-structures/python-lists)
* [Tuples](/what-are-data-structures/python-tuples)
* [Dictionaries](/what-are-data-structures/python-dictionaries)
* [Sets](/what-are-data-structures/python-sets)
* [What is an Iterable?](/what-are-data-structures/iterable-in-python)
* [Map, Filter, Reduce Functions](/what-are-data-structures/python-map-filter-and-reduce-functions)

{% hint style="success" %}

#### Error Handling

The purpose of this section is to provide an explanation and examples of error handling in Python, highlighting its importance and demonstrating various techniques to catch and handle exceptions in Python programs.
{% endhint %}

✅ [Error Handling](/error-handling-in-python)

{% hint style="success" %}

#### Loop...else, partial functions & working with type hints

The purpose of this section is to explain the usage and behaviour of the loop...else clause in Python, providing examples and highlighting its significance in different types of loops (for and while) for better understanding by readers.
{% endhint %}

✅ [Loop...else, Partial Functions & Working with Type Hints](/loop-...-else-clause-partial-functions-and-type-hints)

* [Loop...else](/loop-...-else-clause-partial-functions-and-type-hints)
* [Partial Functions](/loop-...-else-clause-partial-functions-and-type-hints/partial-functions)
* [Type Hints](/loop-...-else-clause-partial-functions-and-type-hints/type-hints)

{% hint style="success" %}

#### Python modules

The purpose of this section is to provide a step-by-step guide on how to use and import functions from Python modules, highlighting the benefits of organising and reusing code through modularisation.
{% endhint %}

✅ [Python Modules](/python-modules)

{% hint style="success" %}

#### Python libraries & packages

The purpose of this section is to provide an overview of Python libraries and packages, explain how to work with them, and highlight their importance in reusing code and expanding the functionality of Python programs.
{% endhint %}

✅ [Python Libraries & Packages](/python-libraries-and-packages)

* [Python Libraries](/python-libraries-and-packages)
* [Python Packages](/python-libraries-and-packages/python-packages)

{% hint style="success" %}

#### Python Directory & IO

The purpose of this section is to provide a concise overview of using the os module in Python for common directory-related tasks, such as creating directories, changing the current working directory, listing directory contents, and checking if a path is a directory.
{% endhint %}

✅ [Python Directory & IO](/directory-and-io)

* [Python Directory](/directory-and-io)
* [Python IO](/directory-and-io/python-io-module)

{% hint style="success" %}

#### Managing Multiple Python Versions

The purpose of this section is to provide a step-by-step guide on how to install and use pyenv to manage multiple versions of Python on various operating systems.
{% endhint %}

✅ [Using Multiple Python Versions](/pyenv)

{% hint style="success" %}

#### Using virtual environments

The purpose of this section is to provide a concise explanation of what a virtual environment is and its significance in Python development.
{% endhint %}

✅ [Using virtual environments](/virtual-environments/virtualenv)

* [Pipenv](/virtual-environments/pipenv)
* [virtualenv](/virtual-environments/virtualenv)

{% hint style="success" %}

#### Using virtual environments

The purpose of this section is to provide an overview of object-oriented programming in Python, explaining the concepts and syntax involved, and highlighting the benefits and importance of using object-oriented programming in Python development.
{% endhint %}

✅ [Object-Oriented Python](/object-oriented-programming-in-python)

* [Class](/object-oriented-programming-in-python/class)
* [Class Methods](/object-oriented-programming-in-python/class-methods)
* [Inheritance](/object-oriented-programming-in-python/python-class-inheritance)
* [Polymorphism](/object-oriented-programming-in-python/python-class-polymorphism)
* [Special Methods](/object-oriented-programming-in-python/python-special-methods)


# Foreword

<figure><img src="https://1979105360-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCnRQCsYT2vYhaXTxEvZe%2Fuploads%2F6YJDJnf6LeAO1J9Tbfoq%2F5588532.png?alt=media&amp;token=1d82490e-c1fa-4232-8843-f48cfb903b3d" alt=""><figcaption></figcaption></figure>

#### Purpose of this book <a href="#purpose-of-this-book" id="purpose-of-this-book"></a>

The purpose of python for starters is to provide a comprehensive introduction to the Python programming language for people who are new to programming or who have little or no experience with Python.&#x20;

Python For Starters should serve as a beginner guide covering the basics of computer programming and build you up to more advanced concepts, using clear and concise explanations and examples that are easy to understand.

**Some specific goals of this book includes:**

✅ Teaching the fundamentals of programming, such as variables, data types, loops, and control structures.

✅ Introducing the Python syntax and the features of the language that are most commonly used.

✅ Providing practice exercises and challenges to help learners build their skills and confidence.

✅ Encouraging learners to experiment with code and to develop a problem-solving mindset.

✅ Helping learners understand how to use Python to build a variety of different types of programs, such as web applications, data analysis tools, or games.

✅ Overall, the goal of a beginner-friendly Python course is to provide a solid foundation in programming and to give learners the skills and confidence they need to continue learning and growing as developers.

#### About The Author <a href="#about-the-author" id="about-the-author"></a>

<div align="left"><figure><img src="https://1979105360-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCnRQCsYT2vYhaXTxEvZe%2Fuploads%2F4okzvQTCBYUYjsMNFZ4z%2FScreenshot%202022-05-22%20at%2011.01.31%20AM.png?alt=media&amp;token=c7b7f1a4-763b-458e-b3a5-a5c751bfc057" alt="" width="188"><figcaption><p>Marvelous Solomon</p></figcaption></figure></div>

I am the Founder & Chief Solutions Architect at Boltcliq, boasting over 13 years of extensive experience in the field of IT. My specialisation lies in developing innovative solutions for educational, health, and commerce applications.

In addition to my role as a Solutions Architect, I am also a proficient Technical Content Writer, leveraging my expertise in software architecture, systems design, and distributed system design (microservices) to deliver high-quality technical content.

Furthermore, I serve as a trusted Technical Consultant, collaborating with businesses to create tailored solutions that maximise their potential and efficiency. My comprehensive knowledge in software architecture and systems design enables me to offer invaluable insights and guidance to clients, empowering them to achieve their goals.

I am proud to be a Co-founder and Lead Training Specialist of Top Universe, a distinguished community dedicated to empowering individuals in their pursuit of a career in the technology industry. Through our initiatives, we provide comprehensive training and support to help aspiring professionals embark on successful tech journeys.

With a passion for driving technological advancements and a deep commitment to excellence, I consistently strive to deliver exceptional results and foster impactful solutions for my clients and the broader tech community.

{% embed url="<https://solomonmarvel.com/>" %}


# Introduction To Python

<figure><img src="https://1979105360-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCnRQCsYT2vYhaXTxEvZe%2Fuploads%2FQ0B6KjneyYNbOA5qs86g%2F1.png?alt=media&amp;token=71b6b197-f23d-4297-803d-dc9a2fc0ddf2" alt=""><figcaption></figcaption></figure>

#### [What is the Python Programming Language?](broken://pages/lWpbLr2i5fv6rgFInoOq) <a href="#what-is-the-python-programming-language" id="what-is-the-python-programming-language"></a>

Python is a high-level, interpreted programming language. It was first released in 1991 and has since become a popular language for web development, data science, and scientific computing. Python is known for its simplicity, readability, and flexibility, making it a great choice for beginners and experienced developers alike. It has a large and active community of users and developers, and a wealth of libraries and frameworks that make it easy to build complex applications quickly.

There are many things that are important to know when learning the Python programming language. Here are a few key points:

* Python is a high-level, interpreted language. This means that it is easy to learn and read, but it may not run as fast as a lower-level language like C or C++.
* Python is dynamically-typed, which means that you don't need to specify the data type of variable when you declare it.
* Python uses indentation to define code blocks, rather than curly braces like many other languages.
* Python has a large standard library, which means that many common programming tasks, such as connecting to a web server or reading and writing files, can be accomplished with minimal code.
* Python has a large and active community, which makes it a great choice for developing open-source projects.
* Python is widely used in scientific computing, data analysis, artificial intelligence, and web development.
* There are two main versions of Python in widespread use today: Python 2 and Python 3. They are similar, but there are a few key differences between the two, so it's important to use the right version for your project.


# Python Installation

There are a few ways to install Python, depending on your operating system and whether you want to install the latest version or a specific version. Here are some general steps to follow:

* Go to the [Official Python Website](https://www.python.org) and click on the "Downloads" link.
* On the downloads page, you will see a list of the latest stable releases of Python. Choose the version you want to install by clicking on the corresponding download link.
* If you are running Windows, a Python installer will be downloaded to your computer. Run the installer and follow the prompts to install Python, make sure to select the option to add Python to your PATH, so you can run Python from the command line.
* If you are running macOS or Linux, you can also download the Python installer, but there are other options available as well. For example, you can use a package manager (such as Homebrew or apt-get) to install Python. Alternatively, you can build and install Python from source code.

Once Python is installed, you can check the version by running the following command in a terminal or command prompt:

```bash
python --version
```

This will print the version of Python that is installed on your system.


# Variables

#### [What are variables?](broken://pages/4njnUglP9cniY8JcF68l) <a href="#what-are-variables" id="what-are-variables"></a>

A variable in Python is a symbolic name that serves as a reference or pointer to an object. Once an object is assigned to a variable, it can be referred to by that name. However, the data is still contained within the object.

In Python, a variable is a named location in memory that stores a value. Variables are used to store data values in programs. When you create a variable, you specify a name for the variable and a value to store in the variable. You can then use the variable name in your program to refer to the value stored in the variable.

For example, you might create a variable named x and assign it the value 10. You can then use the variable x in your program to refer to the value 10.

```
# In this example, the variable x is created and assigned the value 10. 
# You can then use the variable x in your program like this:
variable = 1

# You can also assign a new value to a variable using 
# the assignment operator (=). For example:
variable = 2

# A variable with a type hint
variable_with_type: int = 1

print(variable_with_type) # print the result of our variable
```

#### [Variable Rules in Python](broken://pages/4njnUglP9cniY8JcF68l) <a href="#variable-rules-in-python" id="variable-rules-in-python"></a>

* A variable must start with a letter or the underscore character.
* A variable cannot start with a number.
* A variable can only contain alphanumeric characters and underscores (A-z, 0-9, and \_ )
* Variables are case-sensitive (age, Age and AGE are three different variables)


# Python Data Types

#### [Data Types](broken://pages/WUC4DFlLKzb8F3hSLYzB) <a href="#data-types" id="data-types"></a>

For short, a data type describes a set of possible values and operations that can be performed on a variable

In Python, a data type is a category for values. Every value in Python has a data type, which determines the kind of operations that can be performed on that value and the way the value can be stored in memory.

There are several built-in data types in Python, including:

* Numbers: Python has three types of numbers: integers (int), floating-point numbers (float), and complex numbers (complex). Integers are whole numbers, while floating-point numbers have decimal points. Complex numbers have a real and imaginary part.
* Strings: A string is a sequence of characters, represented by either single quotes (') or double quotes ("). Strings can contain letters, numbers, and special characters.
* Lists: A list is an ordered collection of values that can be of any data type. Lists are written as a series of values separated by commas and enclosed in square brackets.
* Tuples: A tuple is similar to a list, but it is immutable, meaning that its values cannot be modified once it is created. Tuples are written as a series of values separated by commas and enclosed in parentheses.
* Sets: A set is an unordered collection of unique values. Sets are written as a series of values separated by commas and enclosed in curly braces.
* Dictionaries: A dictionary is a collection of key-value pairs. The keys must be unique and are used to retrieve the corresponding values. Dictionaries are written as a series of key-value pairs separated by commas and enclosed in curly braces.

In addition to these built-in data types, you can also define your own data types in Python using classes (more of this in an advanced topic)


# Python Data Structures

[**Data Structures**](broken://pages/NmITLNLexxV7WYBTBaGS)

In computer science, a data structure is a way of organizing and storing data in a computer so that it can be accessed and modified efficiently. Python has a number of built-in data structures, which can be used to store and organize data efficiently.

Python's basic data structures include list, set, tuples, and dictionary. Each data structure is distinct in its own way. Data structures are "containers" that organize and categorize data. The mutability and order of the data structures differs from one data structure to the other.

Some of the most common data structures in Python include:

* Lists
* Tuples
* Dictionaries
* Sets
* Strings


# Python numbers

#### [Numbers](broken://pages/j1c0z2APMJ19QosL6O12) <a href="#numbers" id="numbers"></a>

Python has three built-in numeric data types: integers, floating-point numbers, and complex numbers. In this section, you'll learn about integers and floating-point numbers, which are the two most commonly used number types. You'll learn about complex numbers in a later section.

```
# numbers.py - file name

number = 1
decimal_number1 = .5
decimal_number2 = 10.5

# you can use the underscores to make the numbers more visible when coding
# The underscores will not be added to the output
salary = 21_500_000

pi = 3.142
r = 5

# simple arithmetic operation using python number type
area = pi * r ** r
print(area)
```

To run the code, simply open your `terminal` for Mac OS users, `command prompt` or `powershell` for Windows OS users and run the following command:

```
python numbers.py
```


# Python strings

[**String**](broken://pages/Og0n2yHmtBrrfZUf2kQE)

A string is a collection of characters wrapped in a single or double quote. Like many other popular programming languages, strings in Python are arrays of bytes representing Unicode characters.

```
first_name = "John"
last_name = "Doe"

# String formating patterns (Different ways to format your python strings)
full_name1 = first_name + " " + last_name
full_name2 = f'{first_name} {last_name}'
full_name3 = '{} {}'.format(first_name, last_name)

print(full_name1) # print the value of full_name1
print(full_name2) # print the value of full_name2
print(full_name3) # print the value of full_name3
```


# Python Boolean

Python boolean type is one of the built-in data types provided by Python, which represents one of the two values i.e. True or False. Generally, it is used to represent the truthy or falsy value of the expressions

```
isMarried = True

print(type(isMarried))

if(not isMarried):
    print("John is not Married")
else:
    print("John is married")
```


# Python constants

#### [Constants](broken://pages/BFRXKWMduNFQglb78vGZ) <a href="#constants" id="constants"></a>

In Python, a constant is a variable whose value remains unchanged throughout the program. Constants are usually defined in all capital letters so that they can be easily identified.

In Python, there is no built-in support for constants. However, you can define constants in your program by assigning a value to a variable and then not changing the value of the variable.

Here is an example of how to define and use constants in Python:

```
"""
Constants -> Python doesn't support constants, 
but we use the uppercase letters to show 
that it is a constant
"""

WEBSITE_HOST = 'https://solomonmarvel.com'

print(WEBSITE_HOST)


# code example

PI = 3.14159265

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

radius = 10
area = calculate_area(radius)
print(f"The area of a circle with radius {radius} is {area}")
```


# Python Comments

Comments in Python is the inclusion of short descriptions along with the code to increase its readability. A developer uses them to write his or her thought process while writing the code. It explains the basic logic behind why a particular line of code was written

```python
# This is a single line comment in python

"""
  This is a doc type/multi line comment
  in python
"""
```


# Type Conversion

#### [Type Conversion](broken://pages/3nluqu32B2kxev0xin5p) <a href="#type-conversion" id="type-conversion"></a>

Type conversion is the process of converting a data type into another data type. Implicit type conversion is performed by a Python interpreter only. Explicit type conversion is performed by the user by explicitly using type conversion functions in the program code. Explicit type conversion is also known as typecasting

```
# file name type_conversion1.py

x: int = 20
y: str = str(x)
```

Run Code:

```
python type_conversion1.py
```

The code returns a `string` type showing that the variable `x` was converted to a `string` type.

Let's try another example:

```
# file_name - type_conversion2.py

name = "John Doe"
conv_name = int(name)

print(type(name))
```

Run Code:

```
python type_conversion2.py
```

The code returns an error `ValueError: invalid literal for int() with base 10: 'John Doe'`.

We got the `ValueError` because the value `John Doe` is not a valid number therefore converting a `string` type to a `number` type is only possible if the `string` can become a valid `number`.


# Control flow in python

#### [Control Flow in Python](broken://pages/rNOOgup7DVXVLgJmdSsV) <a href="#control-flow-in-python" id="control-flow-in-python"></a>

A program's control flow is the order in which the program's code executes. The control flow of a Python program is regulated by conditional statements, loops, and function calls.

[**If statement in python**](broken://pages/rNOOgup7DVXVLgJmdSsV)

In a Python program, the if statement is how you perform this sort of decision-making. It allows for conditional execution of a statement or group of statements based on the value of an expression.

```
# example 1: Make a guess game
guess = 5
if(int(guess) == 7):
    print('You won')
    

# example 2: if statement example 2
if 2>5 and 2>1:
    print("One of this condition is correct")


# examle 3: if elif else conditionals
name = "John"

if name == "John":
    print(name)
elif name == "Andrew":
    print('name is not marv')
else:
    print('The name is neither John nor Andrew')

# if statement short hand (tenary operator)    
condition = True if 5 > 5 else False
```

[**For Loop**](broken://pages/rNOOgup7DVXVLgJmdSsV)

A for loop is used for iterating over a sequence (that is either a list, a tuple, a dictionary, a set, or a string). This is less like the for keyword in other programming languages, and works more like an iterator method as found in other object-orientated programming languages.

```
# for loop control flow - example 1
for i in range(2):
    if i == 0:
        continue
    elif i%2 == 0:
        print (f'{i} is an even number')
    else:
        print(f'{i} is an odd number')

name = "Joy"
result = "Name is Joy" if name == "Joy" else "Name is not Joy"
print(result)
```

[**For loop break keyword**](broken://pages/rNOOgup7DVXVLgJmdSsV)

'break' in Python is a loop control statement. It is used to control the sequence of the loop. Suppose you want to terminate a loop and skip to the next code after the loop; break will help you do that. A typical scenario of using the Break in Python is when an external condition triggers the loop's termination.

```
# Using the continue keyword in a loop
for i in range(50):
    if(i == 25):
        break
    print(i)
```

[**For loop continue keyword**](broken://pages/rNOOgup7DVXVLgJmdSsV)

'continue' in Python is a loop control statement. It is used to control the sequence of the loop. Suppose you want to skip a certain item and move to the next in a loop, 'continue' will help you achieve that.

```
# Using the continue keyword in a loop
for i in range(50):
    if(i == 25):
        continue
    print(i)
```

[**While loop in python**](broken://pages/rNOOgup7DVXVLgJmdSsV)

A while loop will run a piece of code while a condition is True. It will keep executing the desired set of code statements until that condition is no longer True. A while loop will always first check the condition before running. If the condition evaluates to True then the loop will run the code within the loop's body.

```
# While loop

max = 5
count = 1
while count < max:
    print("We are live")
    count += 1
```


# Python Functions

### [What is a python function?](broken://pages/6tAmRHp4rJlWd4mmCeLw)

In Python, a function is a block of code that performs a specific task. Functions help you organize and reuse your code, making it more efficient and easier to understand.

Functions can also return multiple values, or no value at all. You can call a function multiple times in your code, and each time it will execute the code inside the function with the given input values.

#### [Here are some important things to note about Python functions:](broken://pages/6tAmRHp4rJlWd4mmCeLw) <a href="#here-are-some-important-things-to-note-about-python-functions" id="here-are-some-important-things-to-note-about-python-functions"></a>

* Functions are defined using the def keyword, followed by the name of the function and a set of parentheses that may include parameters.
* The block of code that makes up the function is indented underneath the definition line.
* Functions may take arguments (also called parameters) that are specified in the parentheses of the function definition. These arguments are used to pass information into the function when it is called.
* Functions may return a value using the return keyword. If a function does not have a return statement, it returns None.
* Functions can be called by using their name followed by a set of parentheses that may include arguments.
* Functions can be defined inside other functions, in which case they are called inner functions. Inner functions can access the variables of the outer function in which they are defined.
* Functions can be passed as arguments to other functions.
* Python has many built-in functions, and you can also define your own custom functions.


# Function Definition

To define a function in Python, you use the def keyword, followed by the name of the function, and a set of parentheses (). The code block within the function is indented.

For example:

```
def call_mum():
    return "Hello Mum!"
```

To call a function, you use the function name followed by a set of parentheses

```
call_mum()
```


# Function Parameter

### [Function with parameter](broken://pages/zFkHXtzUXfzJXGOTc52L) <a href="#function-with-parameter" id="function-with-parameter"></a>

In Python, a function parameter is a variable that is used to pass information into a function when it is called. When you define a function, you can specify the parameters that the function should accept as input. These parameters become variables that you can use within the function to perform your desired task.

```
# This function should return the area of a circle when given the appropriate 

# parameter Pi * r ** 2

PI = 3.142 # a constant, meaning that you should not change it [but of course can be changed eventually]

def area_of_a_circle(radius):
    result = PI * radius ** 2
    return result

area = area_of_a_circle(3)

print(area)
```

### [Working with default parameters](broken://pages/zFkHXtzUXfzJXGOTc52L) <a href="#working-with-default-parameters" id="working-with-default-parameters"></a>

In Python, you can specify default values for function parameters, which will be used if the caller does not provide a value for that parameter. This can be useful if there is a common value that you want to use for a parameter in most cases, but you still want to allow the caller to override it if needed.

To specify a default value for a function parameter, you can include an assignment expression in the parameter list when defining the function

* If a value is passed to the function when called, the value passed will override the default vale

```
PI = 3.142
def area_of_a_circle(radius=2):
    result = PI * radius ** 2
    return result

area = area_of_a_circle()
print(area)
```

### [Working with arguments (\*args)](broken://pages/zFkHXtzUXfzJXGOTc52L) <a href="#working-with-arguments-args" id="working-with-arguments-args"></a>

In Python, the \*args syntax is used to pass a variable number of arguments to a function. It allows you to pass a variable number of arguments to a function as a tuple.

Here is an example of how to use \*args in a function definition:

```
# Collecting Arguments using

def args_get_bio(*args):    
    
    print(args[0])

    print(args[1])
    
    print(args[2])

get_bio = args_get_bio(2022, "john", "solomon", 45, "Nexford")

print(get_bio)
```

### [Working with keyword arguments (\*\*kwargs)](broken://pages/zFkHXtzUXfzJXGOTc52L) <a href="#working-with-keyword-arguments-kwargs" id="working-with-keyword-arguments-kwargs"></a>

In Python, the \*\*kwargs syntax is used to pass a variable-length argument dictionary to a function. It allows you to pass a variable number of keyword arguments to a function as a dictionary.

Here is an example of how to use \*\*kwargs in a function definition:

```

# Collecting Arguments using **kwargs

def kwargs_get_bio(**kwargs):    
    year = kwargs['year']
    first_name = kwargs['first_name']
    print(first_name, year)
    
get_user_bio = kwargs_get_bio(first_name="John", year=2022)
```


# Recursive Functions

### [Recursive Functions in python](broken://pages/FZTMxCvFqLJg92ESXsSh) <a href="#recursive-functions-in-python" id="recursive-functions-in-python"></a>

In Python, a recursive function is a function that calls itself. Recursive functions can be used to solve problems that can be broken down into smaller problems of the same type.

Here is an example of a simple recursive function in Python that calculates the factorial of a number:

```
def factorial_example(n):
    if n == 1:
        return 1
    else:
        return (n * factorial_example(n-1))
factorial_example(5)
```


# Using Global Variables in Functions

### [Python working with global variables](broken://pages/boxzlmv9dGpcibDMpcAd) <a href="#python-working-with-global-variables" id="python-working-with-global-variables"></a>

In Python, a global variable is a variable that is defined outside any function or class, and can be accessed from anywhere in the program. Global variables can be useful for storing values that need to be shared across different parts of your program.

Here is an example of how to define and access a global variable in Python:

```
# Referencing a global variable inside a function
count = 1
def func():
    """
    This function would increament the value of count by 1
    Basically we just added 1 to the count variable
    """

    # specifies that we might change the value of count

    global count 
    count += 1
    return count

print(func.__doc__)
```

* When working with a global variable inside a function, we have to use the `global` keyword to reference the global variable to specify that we want to modify the global variable


# Lambda Functions

### [Working with lambda functions](broken://pages/ThRMeccmWC3HOJJy8COE) <a href="#working-with-lambda-functions" id="working-with-lambda-functions"></a>

In Python, a lambda function is a small anonymous function. It can take any number of arguments, but can only have one expression. Lambda functions are useful when you need a simple function for a short period of time. They are not meant to be used as standalone functions and are not as flexible as regular functions.

The syntax for a lambda function is:

```
lambda arguments: expression
```

This is an example of a lambda function to multiply two numbers:

```
add = lambda a,b : a * b
print(add(5,5))
```

[**There are several scenarios where lambda functions can be useful in Python:**](broken://pages/ThRMeccmWC3HOJJy8COE)

* When you need a small anonymous function for a short period of time.
* When you want to pass a function as an argument to another function (e.g. as a callback).
* When you want to define a function inline (i.e. within another function).
* As a way to avoid creating a function with a def statement when the function is only going to be used once.


# What are Data Structures

In computer science, a data structure is a way of organizing and storing data in a computer so that it can be accessed and modified efficiently. Different types of data structures are suited to different kinds of applications, and some are highly specialized to specific tasks. Some common data structures include:

Although python doesn't implement the above-mentioned data structures out of the box, we can most likely implement them manually.

Some standard data structures in computer science include:

* Arrays: An array is a collection of items that are stored in contiguous memory locations. Each item in the array has a specific index, and the items can be accessed directly by their index.
* Linked lists: A linked list is a collection of items that are stored in separate memory locations, but are linked together using pointers. Each item in a linked list has a data element and a pointer to the next item in the list.
* Stacks: A stack is a data structure that follows the Last In, First Out (LIFO) principle. Items are added to the top of the stack (pushed) and removed from the top (popped).
* Queues: A queue is a data structure that follows the First In, First Out (FIFO) principle. Items are added to the end of the queue (enqueued) and removed from the front (dequeued).
* Trees: A tree is a data structure that consists of nodes arranged in a hierarchy. The top node is called the root, and the nodes below it are called child nodes. Each node in a tree can have zero or more child nodes.
* Graphs: A graph is a data structure that consists of a set of vertices (nodes) and a set of edges that connect the vertices. Graphs can be used to represent a wide variety of real-world relationships, such as social connections, transportation networks, and data dependencies.

Data structures are an important part of computer science because they provide the foundation for efficient algorithms and enable computers to store and manipulate large amounts of data.

## Python Data Structures

In Python, there are several built-in data structures that you can use to store and organize data:

* Lists: Lists are ordered collections of objects. They can store elements of different data types, and are implemented as arrays in Python. Lists are mutable, which means you can change their contents after they have been created.
* Tuples: Tuples are also ordered collections of objects, but they are immutable, which means you cannot change their contents after they have been created. Tuples are often used to store data that should not be modified, such as records in a database.
* Dictionaries: Dictionaries are unordered collections of key-value pairs. They are implemented using hash tables in Python, and are very efficient for looking up values based on their keys.
* Sets: Sets are unordered collections of unique elements. They are useful for storing data that does not need to be in a particular order, and for removing duplicates from a list.
* Strings: Strings are sequences of characters. They are immutable, which means you cannot change the contents of a string after it has been created. Strings are often used to store and manipulate text data.

In addition to these built-in data structures, Python also has several modules in its standard library that provide additional data structures, such as the `collections` module, which includes other specialized data structures.

You can use these data structures to store and organize data in your Python programs. For example, you might use a list to store a set of names, a tuple to store a record of information about a person (such as their name, age, and address), a set to store a list of unique items, or a dictionary to store data about a set of objects (such as a set of employees and their salaries).


# Python Lists

In Python, a list is an ordered collection of items. Lists can contain items of any data type, including numbers, strings, and even other lists.

### Creating a List

Lists are created using square brackets \[ ] and the items are separated by commas.

```python
fruits = ['apple', 'banana', 'orange', 'mango']
print(fruits)
```

### Accessing List items

You can access the items in a list using their indexes. The indexes start at 0 for the first item, 1 for the second item, and so on.

```python
fruits = ['apple', 'banana', 'orange', 'mango']
first_fruit = fruits[0]
print(first_fruit)
```

You can also use negative indexes to access items from the end of the list

This is an example:

```python
fruits = ['apple', 'banana', 'orange', 'mango']
last_fruit = fruits[-1]
next_to_last_fruit = fruits[-2]
print(last_fruit)
print(next_to_last_fruid)
```

### Append item to a List

We can append items into a list using the following pattern `.append(value)` method adds items to the tail of our list

```python

list = ["John", "Abraham", "Mark", "Trust"]
print(list) # list before append
list.append("Joy")
print(list) # list after append
```

### Insert item in List

We can insert items into a list using the following pattern `.insert(index, value)`. Here, you have to specify the index (i.e. the position) that you intend adding the item to.

```python
list = ["John", "Abraham", "Mark", "Trust"]
list.insert(3, "Marrieth")
print(list)
```

### Removing items from a list

There are variable numbers of ways to remove items from a list, we can remove items from the tail of our list using the `.pop()` command, using the index of the item or even using the value of the item.

#### Removing the last item from the list using `.pop`

```python
list = ["John", "Abraham", "Mark", "Trust"]
print(list) # before .pop() operation
list.pop()
print(list) # after .pop() operation
```

#### Removing items using the item index & the delete keyword `del list[index]`

```python
# removing an item using the index
list = ["John", "Abraham", "Mark", "Trust"]
del list[1]
print(list)
```

#### Removing items using the value of the item `list.remove(value)`

```python
list = ["John", "Abraham", "Mark", "Trust"]
list.remove("John")
print(list)
```

### Finding the index of an element in a list

In Python, a list index refers to the position of an element in a list. A list is an ordered collection of items, and each item has a corresponding index that indicates its position in the list.

```python
names = ["John", "Mark", "Marrieth"]
print(names.index('Mark'))
```

### Sorting a python list

Sorting is the process of rearranging the items in a list in a particular order. In Python, you can use the `sorted()` function to sort a list. By default, the sorted() function sorts the items in ascending order.

We use the `.sort()` and `.sort(reverse=True)` to sort our python list.

Sorting items in ascending order:

```python
list_of_names = ["Emmanuella", "Joy", "John", "Abraham"]

# sorting our list in ascending order

list_of_names.sort()
print(list_of_names)
```

Sorting items in descending order:

```python
list_of_names = ["Emmanuella", "Joy", "John", "Abraham"]

# sorting our list in ascending order

list_of_names.sort(reverse=True)
print(list_of_names)
```

### Slicing a list in python

In Python, you can use slicing to extract a portion of a list (also known as a sublist). Slicing uses a start index, a stop index, and an optional step size to specify the elements that should be included in the slice.

You can use slicing to modify the elements in a list by assigning new values to the slice. For example, you can use slicing to delete elements from a list by assigning an empty list to the slice:

```python
names = ["Emmanuella", "Joy", "Prince", "Melvina", "Mercilia"]

# slicing items from index 1 through 4

print(names[1:4])

# slicing items from index 0 to -2

print(names[:-2])
```

### Unpacking a list in python

In Python, you can use unpacking to assign the elements of a list to separate variables. This can be a convenient way to assign multiple variables at once, rather than assigning them one by one.

Example of list unpacking:

```python
names = ["John", "Mark", "Marrieth"]
john, mark, marrieth = names
print(john, mark, marrieth)

# You can use the (*) to unpack the left overs into another list

john, *others = names
print(others)
```

### Looping through a python list

Looping is a programming construct that allows you to repeat a block of code a certain number of times or until a certain condition is met. In Python, there are two types of loops: `for loops` and `while loops`.

A for loop is used to iterate over a sequence (such as a list, tuple, or string) or an iterable object (such as a dictionary, file, or set). The for loop will execute a block of code for each element in the sequence, and it will automatically iterate through all the elements in the sequence.

You can use loops to perform a wide variety of tasks in Python, such as iterating over a list of items, searching for a particular element in a list, or repeating a set of actions until a certain condition is met. Loops are an essential part of programming and are used extensively in Python and other programming languages.

There are several ways to loop through a list in Python. Here are a few examples:

Using a `for` loop:

```python
names = ["John", "Mark", "Marrieth"]

for i in names:
    print(i)
```

Using a `while` loop:

```python
names = ["John", "Mark", "Marrieth"]
while i < len(names):
    print(numbers[i])
    i += 1
```

Using the `.enumerate()` function:

```python
names = ["John", "Mark", "Marrieth"]
while i, name in enumerate(names):
    print(i, name)
```

Using a list comprehension:

```python
names = ["John", "Mark", "Marrieth"]
[name for name in names]
```

Each of these techniques has its own use cases, and you can choose the one that is most appropriate for your needs. For example, you might use a `for` loop if you want to perform a specific action for each element in the list, or a list comprehension if you want to create a new list based on the elements in the original list.

### Python List Comprehension

List comprehension is a concise way to create a list using a single line of code. It is a powerful tool for creating lists based on existing lists or other iterable objects.

List comprehension has the following syntax: `[expression for item in iterable]`

```python
numbers = [1, 2, 3, 4, 5]

# This is an example iterating through a list and checking for a condition
result = [i for i in numbers if i % 2 == 0]
print(result)
```

List comprehension is a concise and efficient way to create lists in Python, and it is often used to create lists that are based on complex transformations or filters of other lists or iterable objects.


# Python Tuples

In Python, a tuple is an immutable sequence type. This means that once you create a tuple, you can not change the values it contains: you can not add, remove, or modify the values of the elements in the tuple.

You can also use the `len()` function to get the length of a tuple, and the in operator to check if an element is in a tuple.

#### Creating a tuple

Tuples are defined using parentheses, with the elements separated by commas.

This is an example of a tuple:

```python
names = ("john", "mark", "marrieth")
print(names)
```

#### Accessing Values in tuples

We can access the values of a tuple by using the square bracket and the index as found in the example below:

```python
names = ("john", "mark", "marrieth")
print(names[0])
```

#### Iterating through a tuple

A tuple is an iterable, this means that we can iterate through the values of a tuple using same pattern as we have seen in lists.

This is an example of how to iterate within a tuple

```python
names = ("john", "mark", "marrieth")
for name in names:
    if name == "Abraham":
        print('Abraham was found')
    print("{} is a name".format(name))
```

#### Returning tuples from function

Tuples are often used to return multiple values from a function, since they allow you to return multiple values in a single return statement.

```python
def get_user():
  name = 'John'
  age = 40
  return name, age

person = get_user()
print(person)  # Output: ('John', 40)

# You can also unpack the tuple into separate variables
name, age = get_user()
print(name)  # Output: 'John'
print(age)   # Output: 40
```


# Python Dictionaries

A dictionary is a built-in data type in Python that stores data in key-value pair format. It is similar to a list or an array in that it stores a key-value pair at each index rather than a single value. A dictionary's keys must be unique, and they are used to access the corresponding values.

#### Create a new dictionary

We can create a python dictionary using the open and close curly brackets, with a `key value` combination representing each item in the dictionary. Here's an example of how you can create and use a dictionary in Python:

```python
dict = {
    "name" : "John",
    "age" : 40,
    "department": "Software Engineering"
}
print(dict)
```

#### Adding a new key value pair

We can add items to an existing dictionary by wrapping the new item key in a square bracket along with the items value. `dict[key] = value`

This is an example:

```python
dict = {
    "name" : "John",
    "age" : 40
}
dict["job title"] = "Lead Engineer"
print(dict)
```

You can access the values in a dictionary using the keys, which are unique within a dictionary. You can also use the len() function to get the number of key-value pairs in a dictionary, and the in operator to check if a key is in a dictionary.

Furthermore, you can also use the items(), keys(), and values() methods to get a view of the key-value pairs, keys, or values in a dictionary, respectively

#### Fetch items using key

To fetch specific items from a python dictionary, we use the following pattern `dict[key]`

```python
# fetch item using key
name = dict["name"]
age = dict["age"]
department = dict["department"]
job_title = dict["job title"]
```

#### fetch item using the .get()

To fetch specific items from a python dictionary, we use the following pattern `dict[key]`

```python
dict = {
    "name" : "John",
    "age" : 40
}
name = dict.get("name")
age = dict.get("age")
print(name)
print(age)
```

#### Deleting items from a dictionary using the `del` keyword

We can delete items from a dictionary using the `del` keyword following the key enclosed in a square bracket. This is an example of how to delete items in a dictionary.

```python
dict = {
    "name" : "John",
    "age" : 40,
    "department": "Engineering"
}
del dict["name"]
print(dict)
```

#### Iterating through a dictionary

Giving the fact that a dictionary is a `iter` (an iterable type), we can iterate over the items `.items()` while accessing the key and value following the pattern `for key, value in dictionary.items()`.

In this example, we are iterating over a list of dictionaries:

```python
array_of_dict = [
    {
        "id" : '09812hAA',
        "name" : "John",
        "age" : 40,
        "department": "Software Engineering"
    },
    {
        "id" : '09812hAB',
        "name" : "Emmanuella",
        "age" : 16,
        "department": "Software Engineering"
    },
]

# getting the key and the value using .items
for dictionary in array_of_dict: # iterating over the array
    for key, value in dictionary.items(): # iterating over the dictionary items
        print(f'{key} -> {value}')
```

#### Dictionary comprehension

A dictionary comprehension is a concise way to create a dictionary. It is similar to a list comprehension, but it returns a dictionary instead of a list.

```python
stocks = {
    'AAPL': 121,
    'AMZN': 3380,
    'MSFT': 219,
    'BIIB': 280,
    'QDEL': 266,
    'LVGO': 144
}

new_stocks = {name: value * 1.2 for (name, value) in stocks.items()}

print(new_stocks)
```

Dictionaries are useful for storing data that needs to be quickly retrieved using a unique key. They are also used to implement many other data structures in Python, such as sets and graphs.


# Python Sets

In Python, a set is an unordered collection of unique elements. Sets are used to store multiple items in a single variable. Sets are useful when you need to keep track of a collection of elements, but don't care about the order, or when you want to eliminate duplicates.

* Elements in a set are unordered.
* Elements in a set are unique. A set doesn’t allow duplicate elements.
* Elements in a set cannot be changed. For example, they can be numbers, strings, and tuples, but cannot be lists or dictionaries.

#### Creating a set in python

In Python, you can create a set using the `set()` function or using curly braces `{}`. Note that using curly braces `{}` to create a set will only work if you have a list of elements. If you try to create a set from a list using curly braces, you will actually create a dictionary.

```python
unique_user = {"john", "emmanuella", "faith", "linux"}
print(unique_user)
```

To create a set from a list of elements, you should use the set() function or curly braces {}.

#### Creating an empty set

To create an empty set, you should use the set() function.

```python
unique_user = set()
print(unique_user)
```

#### Clear a set

To remove all the items in a set, we can use the `.clear()` method to achieve this.

```python
unique_user = {"john", "emmanuella", "faith", "linux"}
unique_user.clear() # cleared the set
print(unique_user)
```

#### Remove items from a set

When removing items from a set, there are different ways to achieve this.

**Removing items using `.remove(key)`**

The `.remvoe()` method removes an item from the set. This operation would throw an error if the item is not found in the set

This is an example:

```python
unique_user = {"john", "emmanuella", "faith", "linux"}
unique_user.remove("john")
print(unique_user)
```

**Removing items using `.discard(key)`**

When using discard, it checks if the item exists in the set before removing the item

This is an example:

```python
unique_user = {"john", "emmanuella", "faith", "linux"}
unique_user.discard("john")
print(unique_user)
```

**Removing items using `.pop()`**

In Python, the `set.pop()` method removes and returns an arbitrary element from the set. If the set is empty, it raises a `KeyError`.

This is an example:

```python
person = {"john", "emmanuella", "andrew", "zack", "melvina", "mercilia"}
person.pop()
print(person)
```

#### Add items to a set

We can add items to a set using the `.add(value)` method

This is an example:

```python
unique_users = set() # initialised an empty set
unique_users.add("mark") # add item "mark" to the set
unique_users.add("john") # add item "john" to the set
print(unique_users)
```

#### Set Comprehension

A set comprehension is a concise way to create a new set from an iterable. It is similar to list comprehensions and dictionary comprehensions, but it creates a set instead of a list or dictionary.

Here is the syntax for a set comprehension:

```python
{expression for item in iterable}
```

Here is an example of a set comprehension:

```python
unique_user = {"john", "emmanuella", "faith", "linux"}
uppercase = {user.upper() for user in unique_user}
print(uppercase)
```

Set comprehensions are a concise and efficient way to create sets in Python. They are also a good way to learn about the syntax and usage of comprehensions in Python.

#### Set Union

In Python, you can use the union() method to find the union of two sets. The union of two sets is a new set that contains all the elements from both sets, without any duplicates.

```python
users1 = {'John', 'Melvina'}
users2 = {'Amaka', 'Emmanuella', "John"}
union = users1.union(users2)
print(union)
```

We can also use the `|` operator to find the union of two sets. This is known as the pipe operator.

```python
users1 = {'John', 'Melvina'}
users2 = {'Amaka', 'Emmanuella', "John"}
union = users1 | users2
print(union)
```

Both of these methods will return a new set that contains all the elements from both sets, without any duplicates.

#### Set Intersection

In Python, you can use the intersection() method to find the intersection of two sets. The intersection of two sets is a new set that contains only the elements that are common to both sets.

Here is an example of set intersection in python:

```python
users1 = {'John', 'Melvina'}
users2 = {'Amaka', 'Emmanuella', "John"}
intersection = users1.intersection(users2)
print(intersection)
```

We can also use the `&` operator to find the intersection of two sets. This is known as the ampersand operator.

```python
users1 = {'John', 'Melvina'}
users2 = {'Amaka', 'Emmanuella', "John"}
intersection = users1 & users2
print(intersection)
```

#### Set Difference

In Python, you can use the difference() method to find the difference of two sets. The difference of two sets is a new set that contains the elements that are in the first set, but not in the second set.

Here is an example showing a set difference:

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella', "John"}
difference = users1.difference(users2)
print(difference)
```

You can also use the `-` operator to find the difference of two sets. This is known as the subtraction operator.

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella', "John"}
difference = users1 - users2
print(difference)
```

#### Set Symmetric\_difference

In Python, you can use the symmetric\_difference() method to find the symmetric difference of two sets. The symmetric difference of two sets is a new set that contains the elements that are in one set or the other, but not both.

Here is an example showing a set symmetric\_difference:

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella', "John", "Konan"}
symmetric_difference = users1.symmetric_difference(users2)
print(symmetric_difference)
```

You can also use the `^` operator to find the symmetric difference of two sets. This is known as the caret operator.

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella', "John", "Konan"}
symmetric_difference = users1 ^ users2
print(symmetric_difference)
```

#### Set subset

In Python, you can use the `issubset()` method to check if a set is a subset of another set. A set is a subset of another set if all the elements in the first set are also in the second set.

Here is an example showing a set subset:

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella'}
is_subset = users2.issubset(users1)
print(is_subset)
```

You can also use the `<=` operator to check if a set is a subset of another set. This is known as the less than or equal to operator.

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella'}
is_subset = users2 <= users1
print(is_subset)
```

#### Set superset

In Python, you can use the `issuperset()` method to check if a set is a superset of another set. A set is a superset of another set if it contains all the elements in the other set.

Here is an example showing a set superset:

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella'}
is_superset = users1.issuperset(users2)
print(is_superset)
```

You can also use the >= operator to check if a set is a superset of another set. This is known as the greater than or equal to operator.

```python
users1 = {'John', 'Melvina', "Emmanuella", "Joy"}
users2 = {'Melvina', 'Emmanuella'}
is_superset = users1 >= users2
print(is_superset)
```

#### Set disjoint

In Python, you can use the `isdisjoint()` method to check if two sets are disjoint. Two sets are disjoint if they have no elements in common.

Here is an example showing a set disjoint:

```python
users1 = {'John', "Joy"}
users2 = {'Melvina', 'Emmanuella'}
is_disjoint = users1.isdisjoint(users2)
print(is_disjoint)
```

The isdisjoint() method returns True if the two sets are disjoint, and False otherwise.

Note that the empty set is considered to be disjoint with all other sets.

#### Frozen set

A frozen set is an immutable version of a Python set object. It is a set that cannot be modified after it is created. You can create a frozen set by using the frozenset() function

Frozen sets are useful when you need to use a set as a key in a dictionary or as an element in another set, because immutable objects can be used as keys or elements.

```python
unique_user_set = {"john", "emmanuella", "faith", "linux"} # our set is mutable
frozen_unique_users = frozenset(unique_user_set) # our set becomes immutable
print(frozen_unique_users)
```

Frozen sets also support all the methods and operations that regular sets do, such as union, intersection, and difference.


# Iterable in python

In Python, an iterable is an object that can be used in a for loop to iterate over its elements. An iterable must implement the `__iter__` method, which returns an iterator, or the `__getitem__` method, which allows indexing of the object.

Some examples of iterable objects in Python are lists, tuples, strings, and dictionaries.


# Python Map, Filter & Reduce Functions

### Python Map Function

The `map()` function in Python is a built-in function that applies a function to each element of an iterable (such as a list, tuple, or string) and returns a new iterator with the transformed elements.

The `map()` function has the following syntax:

```python
map(function, iterable)
```

This is an example of a list transformation using the `map()` function:

```python
numbers = [1, 2, 3, 4, 5]
# Map (map(function, list))
# block of code to add 10 to each value in our list

result = map(lambda item: item + 10, numbers)
print(list(result))
```

> The `map()` function is a convenient way to apply a function to many elements in an iterable. It is often used to transform or filter the elements of a list or other iterable object.

#### Python Filter Function

The `filter()` function in Python is a built-in function that takes an iterable and a function as input, and returns an iterator with the elements that return True when passed to the function.

The `filter()` function has the following syntax:

```python
filter(function, iterable)
```

This is an example of a list transformation using the `filter()` function:

```python
numbers = [1, 2, 3, 4, 5]
# Filter (filter(function, list))
# write a python code to filter the items that are divisible by 2

result = filter(lambda item: item % 2 == 0, numbers)
print(list(result))
```

> The filter() function is a convenient way to select specific elements from an iterable based on a certain condition. It is often used to filter lists or other iterable objects to create new lists with only the elements that satisfy a certain criterion.

#### Python Reduce Function

The `reduce()` function in Python is a built-in function that applies a function to an iterable in a cumulative manner to reduce the iterable to a single value. It is a part of the functools module, so you need to import it before you can use it.

The reduce() function has the following syntax:

```python
reduce(function, iterable, initializer=None)
```

The `reduce()` function is a function that takes in two arguments and returns a single value. The `'iterable'` is an `iterable` object, such as a list, tuple, or string. The `initializer` is an optional argument that specifies an initial value to be used in the reduction. If the `initializer` is not provided, the `reduce()` function will use the first element of the `iterable` as the initial value.

This is an example of a list transformation using the `reduce()` function:

```python
numbers = [1, 2, 3, 4, 5]
# Reduce (reduce(function, list))
# we use the reduce function to reduce a list into a single value

from functools import reduce

result = reduce(lambda a, b: a + b , numbers)
print(result)
```

> The `reduce()` function is a useful tool for performing a cumulative operation on an iterable object. It can be used to perform a wide variety of tasks, such as calculating the sum or product of a list of numbers, finding the longest string in a list, or concatenating a list of strings.


# Error handling in Python

Error handling is an important part of writing Python programs. It allows you to catch and handle errors and exceptions that may occur in your program, so that it can continue to run smoothly.

There are several ways to handle errors in Python. One of the most common is to use the `try` and `except` statements. Here's an example of how you might use these statements:

```python
try:
    # some code here
except SomeException:
    # code to handle the exception
```

The `try` block contains the code that might throw an exception. If an exception is raised while this code is being executed, the program will jump to the `except` block and execute the code there. If no exception is raised, the `except` block is skipped.

You can also specify multiple exceptions to catch by separating them with a comma, like this:

```python
try:
    # some code here
except SomeException, AnotherException:
    # code to handle the exception
```

You can also use the `finally` block to specify code that should be executed whether an exception is raised or not. This is often used to clean up resources that were being used, such as closing a file or database connection.

```python
try:
    # some code here
except SomeException:
    # code to handle the exception
finally:
    # clean up code
```

You can also raise your own exceptions using the `raise` statement. This is useful when you want to signal that something has gone wrong in your program, and you want to stop execution.

```python
if some_condition:
    raise SomeException("An error occurred")
```

#### Try ... Catch Example

Here's a typical example of how try and except can be used in Python:

```python
number = 10
name = "John"

print(name)

try:
    print(str(number) + name)
    print(name/number)
except Exception as e:
    print(e)

age = 40
print(age + number)
```

#### Try ... Catch ... Finally Example

```python
joy = "Joy"
ella = "Emmanuella"

try:
    value = len(joy)/ella # this will throw an error
    print(value)
except Exception as e:
    print("Error: ", e)
    value = len(joy)/len(ella)
    print("Error resolution" + " " + str(value))
finally:
    print("Finally, We implemented error handling here")
```

#### Try ... Catch ... else Example

In Python, the `try` and `except` statements are used to handle exceptions, which are runtime errors that can occur when executing a program. The `try` block contains code that may throw an exception, and the `except` block contains code that will be executed if an exception is thrown.

The `else` clause is optional and is used to specify a block of code that should be executed only if the `try` block **does not throw an exception**

```python
try:
    value = len(joy)/ella # this will throw an error
    print(value)
except Exception as e:
    print("Error: ", e)
    value = len(joy)/len(ella)
    print("Error resolution" + " " + str(value))
else:
    print("This would print if there's no error")
```


# Loop ... Else Clause, Partial Functions & Type Hints

## Loop ... Else Clause

In Python, the `else` clause in a loop is executed when the loop terminates normally (when a break statement is not executed). This is in contrast to the `try` and `except` statements, where the `else` block is executed when no exception is raised.

#### For... else

In Python, the for statement is used to iterate over a sequence (such as a list, tuple, or string) or other iterable object. The `else` clause in a for loop is optional and is executed only if the loop completed normally (that is, if the loop was not interrupted by a break statement).

* The `else` clause would execute if our iteration or loop executes normally without breaking
* The `else` clause will also execute if the loop is empty

```python
items = [1 , 2]

for i in items:
    print("number", i)
    
    # if we break the loop at some point the else would not work
    if(i == 2):
        break
else:
    print("The loop is either empty or did not run normally")
```

#### While... else

In Python, the while loop executes a block of code repeatedly as long as a certain condition is true. The `else` clause in a while loop in Python is optional and is executed only when the condition in the while loop becomes False. It is not executed if the loop is terminated by a break statement.

```python
items = [1, 2, 3, 4, 5]
count = 0

while count < len(items):
    count = count + 1
    if(count == 4):
        break
    print(count)
else:
    print("The loop is either empty or did not run normally")
```

The `else` clause in a loop can be a useful way to add some additional logic that should be executed only if the loop completes normally. However, it's important to note that the `else` clause is not the same as the `else` clause in a `try` and `except` block. The `else` clause in a `try` and `except` block is executed when no exception is raised, whereas the `else` clause in a loop is executed when the loop terminates normally.


# Partial Functions

In Python, a partial is a function that allows you to fix some arguments of another function. This is useful when you need to use a function with a fixed set of arguments in multiple places, but the arguments that you want to fix are not always the same.

For example, suppose you have a function that takes three arguments:

* A partial function returns a new partial object that is callable by another function

Here's an example of how to work with Partial Functions

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

def double(a):
    return multiply(a, 2)

result = double(10)
print(result) 
```

#### Using partials from the python in-built functools library

```python
# import partials

# example 1
from functools import partial

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

number = 2
partial_function = partial(multiply, number) # creates a partial

result = partial_function(10)
print(result)

# example 2

from functools import partial

def foo(x, y, z):
    return x + y + z
    
foo_with_y_fixed = partial(foo, 5)

print(foo_with_y_fixed(1, 2))  # prints 8
print(foo_with_y_fixed(3, 4))  # prints 12
```

NB: Note that when you create a partial, you need to specify the arguments in the order in which they appear in the original function. If you want to specify arguments by name, you need to use the functools partial method function.


# Type Hints

Type hints in Python are a way to specify the expected type of function's arguments and return value. They are a form of documentation that can be used to help developers understand and use the code, and they can also be used by static type checkers to detect type errors in the code.

They are completely optional and are not used by the interpreter to enforce any type checking. Instead, they are mainly used as a form of documentation to help other developers understand the code.

* Python’s type hints provide you with optional static typing to leverage the best of both static and dynamic typing.

Here's an example of type annotations in your python code:

```python
"""
The type descriptions are optional but it makes it 
easier for other developers to read your code and 
understand the types that you're working with
"""

name: str = "Marvelous"
number: int = 10

# using types to descript the parameter types and the function return type
def say_hi(name: str) -> str:
    return f'Hi {name}'

greeting = say_hi('Emanuella')
print(greeting)
```

Type annotations can be especially useful when working with large codebases or when collaborating with other developers. They can help to make the code more self-explanatory and easier to understand.

It's worth noting that type annotations are a relatively recent addition to Python (introduced in Python 3.0). Prior to the introduction of type annotations, developers used comments or documentation strings to describe the expected types of variables and expressions.


# Python Modules

In Python, a module is a file that contains definitions of functions, classes, and variables, and can be imported into other Python files or programs. Modules are used to organize code and make it more reusable and easier to maintain.

* Using modules is a good way to organize and reuse your code, and it's a common practice in Python programming.

We are going to learn how to import functions from other modules (files) in the following steps:

Create a python file named `circle.py`, then add the following code to the file

```python
# circle module

PI = 3.142

def area_of_a_circle(r):
    """
    Function for computing the area of a circle
    """
    area = PI * r ** 2
    return area

```

Create a python file named `rectangle.py`, then add the following code to the file

```python
# rectangle module

def area_of_a_retangle(l, b):
    """
    Formula for computing the area of a rectangle
    """
    return l * b
```

Now create a new file names `main.py`. This is our main file and our program execution would begin from here.

We will import the circle and rectangle files using the `import` statement

```python
"""
For shorts, a module is simply a python file
The python file can contain classes, functions and even variables

And the classes, functions or variables can be imported to other files
"""


from circle import area_of_a_circle # import area_of_a_circle function
from rectangle import area_of_a_retangle

# called the area of a circle function
print(area_of_a_circle(10))

# called the area of a rectangle function
print(area_of_a_retangle(5, 5))
```

#### Working with module files and subdirectories

When working with multiple directories (folders), accessing your modules would be a quite different approach

* Create a finder directory (folder) within your current directory
* Create a `sayHelloy.py` and `sayHi.py` file.

Within the `sayHi.py`, paste the following code:

```python
def sayHi(name):
    return f'Hi, {name}'
```

Within the `sayHello.py`, paste the following code:

```python
def sayHello(name):
    return f'Hello, {name}'
```

Now, let's access the `sayHello` and `sayHi` functions within those files from our main file using the following block of code:

```python
# import sayHi function
from functions.sayHello import sayHello
from functions.sayHi import sayHi

print(sayHello("Marv"))
print(sayHi("Joy"))
```


# Python Libraries & Packages

### Working with Python Libraries

A Python library is a collection of modules that contain functions and classes that you can use in your code. You can import a library into your code using the `import` statement, and then use the functions and classes defined in the library by calling them using dot notation. For example, you might import the math library and use the `sqrt` function like this:

```python
import math

x = math.sqrt(25)
print(x)
```

Here, the `sqrt` function returns the square root of its argument. There are many standard libraries included with Python, and you can also install third-party libraries using the `pip` package manager.

Python has a large standard library that you can use right away.

If you require a package that is not included in the standard library, you can look it up on the Python Package Index.

The largest Python repository is the `Python Package Index (PyPI)`. It includes many Python packages created and maintained by the Python community.

You can use the search box to find a package. For example, you can use the `requests` keyword to find packages that deal with HTTP requests.

Many relevant packages will be displayed in the search results. You can get more information about each package by clicking the corresponding link.

You can install the python *request* library using the `pip` command globally or in a virtual environment using the command below:

```bash
pip install requests
```

This library would be downloaded from the <https://pypi.org> repository.

Then try it out by creating a new python file called main.py and entering the code block bellow:

```python
import requests

response = requests.get('https://pypi.org/')

# This would return the status code of the website request
print(response.status_code)
```

#### More on python libraries

Libraries are useful because they provide a way for you to reuse code that has already been written and tested, saving you the time and effort of writing that code yourself.

There are many Python libraries available, covering a wide range of functionality. Some common libraries include:

* `numpy`: a library for working with large, multidimensional arrays and matrices of numerical data
* `pandas`: a library for working with data in a tabular form, such as data frames
* `matplotlib`: a library for creating static, animated, and interactive visualizations
* `scikit-learn`: a library for machine learning tasks, including classification, regression, and clustering
* `requests`: a library for making HTTP requests to web servers

Then, you can import the library into your program using the import statement. For example, to use the NumPy library, you would include the following line at the top of your Python script:

```python
import numpy as np # as alias np
```

This imports the NumPy library and gives it the alias `np`, so that you can refer to it as `np` in your code. You can then use functions and other resources from the library by calling them using the `np.function_name()` syntax.


# Python Packages

## Packages

A Python package is a directory of Python modules. Packages are a way to organize your code by grouping related modules together. For example, you might have a package called `mypackage` that contains several modules, each of which defines a specific function or class.

#### Creating a package in python

To create a package in Python, you will need to follow these steps:

* Create a directory for your package, with a descriptive name that reflects the contents of the package.
* Inside the package directory, create a file named `__init__.py`. This file can be empty, or it can contain code that runs when the package is imported.
* Create one or more Python modules (i.e., files ending in `.py`) that contain the code you want to include in the package.
* Optionally, create a setup.py file at the top level of your package. This file can be used to specify metadata about your package, such as its name, version, and dependencies, and to automate the process of installing and distributing your package.
* Optionally, create a README.md file that describes the contents and purpose of your package, and any other relevant information.

For example, suppose you want to create a package called `my_package` that contains a module `my_module.py`. Here's how you might structure your package:

```
my_package/
├── __init__.py
├── my_module.py
└── setup.py
```

To use your package in another Python script, you would import it using the following syntax:

```python
import my_package.my_module
```

You can then access the contents of the package using the dot notation:

```python
my_package.my_module.specific_function()
```


# Directory & IO

## Working with Directories

In Python, you can use the `os` module to work with directories (also called "folders"). The `os` module provides functions to perform various operations on directories.

Here are a few examples of common tasks you might perform with the `os` module:

**Creating a Directory**

To create a new directory, you can use the `os.mkdir()` function. This function takes a single argument, which is the name of the directory you want to create.

```python
import os

# Create a new directory called "newdir"
os.mkdir("emmanuella")
```

**Changing the Current Working Directory**

To change the current working directory, you can use the `os.chdir()` function. This function takes a single argument, which is the path of the directory you want to change to.

```python
import os

# Change the current working directory to "newdir"
os.chdir("emmanuella")
```

**Listing the Contents of a Directory**

To list the contents of a directory, you can use the `os.listdir()` function. This function takes a single argument, which is the path of the directory you want to list the contents of. It returns a list of the names of the files and directories in that directory.

```python
import os

# List the contents of the current working directory
contents = os.listdir()
print(contents)
```

**Checking if a Path is a Directory**

To check if a given path is a directory, you can use the `os.path.isdir()` function. This function takes a single argument, which is the path you want to check. It returns `True` if the path is a directory, and `False` if it is not.

```python
import os

# Check if the current working directory is a directory
is_dir = os.path.isdir(".")
print(is_dir)
```


# Python IO Module

In Python, the `io` module provides a uniform interface for reading and writing streams of data. It is part of the Python Standard Library, which means it is available to use in any Python program without the need to install additional packages.

The `io` module defines several classes that can be used to read and write data in various formats, including text, binary, and raw data. These classes include `TextIOWrapper`, `BufferedReader`, and `BufferedWriter`, among others.

One common use of the `io` module is to read and write files. For example, you can use the open() function from the `io` module to open a file, and then use methods like `read()` and `write()` to read from and write to the file. Here is an example of reading from a file and printing its contents to the console:

The `mode` is an optional parameter. It’s a string that specifies the mode in which you want to open the file. Below, we will show available modes for opening a text file:

* `'r'` Open for text file for reading text
* `'w'` Open a text file for writing. If the file exists, the function will truncate all the contents as soon as you open it. If the file doesn’t exist, the function creates a new file.
* `'a'` Open a text file for appending text. If the file exists, the function append contents at the end of the file.
* `'+'` Open a text file for updating (both reading & writing).
* `'x'` Create a file and write to the file

You can also use the `io` module to read and write data from other sources, such as network sockets or in-memory buffers. The `io` module provides a consistent interface for working with these different types of data streams, making it easier to write code that is portable across different platforms and environments.

In this example `io` operations we are using a context manager to manage the context of the read operation:

```python
import io

# Open the file in read mode

with io.open('filename.txt', 'r', encoding='utf-8') as f:
  # Read the contents of the file
  contents = f.read()
  
  # Print the contents
  print(contents)
```

### What is a context manager?

The `with` keyword is used when creating a context manager i.e. managing the context of the ongoing operation (in this case a file open operation).

A context manager is an object that defines the methods `__enter__` and `__exit__`, which allow you to execute code before and after a block of code, respectively. Context managers are used in the with statement, which ensures that the code in the block is executed within the context of the manager.

Context managers are very useful because they allow you to manage resources, such as files or database connections, in a structured way. They ensure that resources are properly cleaned up when they are no longer needed, which can help prevent errors and improve the performance of your code.

#### Reading from file

In this example, we are reading from a file:

First create a file `read.txt` in the location/directory, then add the following lines of text:

```
Hello
World!
```

The following code block will read the `read.text` file and print the result to your terminal console.

```python
# reading from a file
with open('read.txt', 'r') as f:
    lines = f.readlines() # read the lines in the file
    for i in lines:
        print(i)
    f.close() # close the file
```

#### Writing to a file

In this example, we are writing to a file:

* First create a file `write.txt` in the location/directory:

The following code block will write the items in the list to the file we have just created.

```python
# writing to a file
lines = ['My name is Trust', 'I am a software engineer', 'Learning to become a data scientist']
with open('write.txt', 'w') as f:
    for line in lines:
        # write the line to the file
        f.write(line)
        f.write('\n')
        # You can check the write.md to see the result
```

#### Creating and writing to a file using the `'x'` mode

In this example, we are creating a file if the file does not exist, and we are writing to the file at the same time:

```python
with open('ellajoyifeoma.txt', 'x') as f:
    f.write('Ella, Joy and Ifeoma attended class today')
```

#### Checking if a file exist using `os.path.exists(filename)`

We can check if a file exist before proceeding to make any computational operation with the file using the `os.path.exists(filename)` command.

Here is an example to do that:

```python
import os.path

# check if the file exist
file_exists = os.path.exists('readme.txt')

if(file_exists):
    print("File exists")
else:
    print("File does not exist")
```

#### Python CSV Module

The `csv` module in Python provides functions for reading and writing CSV (comma-separated value) files.

**Reading from a CSV file**

Here's an example of how to use `csv.reader` to read a CSV file and print each row:

```python
import csv

with open('data.csv', 'r') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
```

This code will open the file `data.csv` in read mode, create a `csv.reader` object, and iterate over the rows in the file. Each row is returned as a list of values, with the values in each column separated by commas.

NB: Add a sample csv file called `data.csv` to the directory.

You can also use the `csv.DictReader` class to read the CSV file into a list of dictionaries, with the keys of the dictionary being the column names and the values being the cell values.

```python
import csv

with open('data.csv', 'r') as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row)
```

This code will print each row as a dictionary, with the keys being the column names and the values being the cell values.

**Writing to a CSV file**

Here's an example of how to write to a csv file using python:

```python
import csv  

header = ['country', 'area code', 'city', 'state']
data = ['Nigeria', 200211, 'AGB', 'OG']

with open('data.csv', 'w', encoding='UTF8') as f:
    writer = csv.writer(f)
    
    # write the header
    writer.writerow(header)
    
    # write the data
    writer.writerow(data)
```

#### Renaming files using `os.rename(filename, new_name)` command

To rename a python file using the `os` module, we have to use the `os.rename(filename, new_name)` where the filename describes the current name of the file including the file extension e.g. `data.csv` and the `new_name` describes the name you want to change the `filename` to.

Here is a simple python code to achieve this operation with ease:

```python
import os

try:
    os.rename('data.csv', 'file.csv')
except FileNotFoundError as e:
    print(e)
```

This will successfully rename the `data.csv` file to `file.csv`.

#### Deleting files using the `os.remove(filename)` command

To delete a file using the `os` module, we can simply use the `os.remove(filename)`.

Here is a python snippet to achieve the `os.remove` operation:

```python
import os

try:
    os.remove('data.csv')
except FileNotFoundError as e:
    print(e)
```

This will successfully delete the `data.csv` file from the directory.


# Pyenv

## Using multiple python versions using pyenv

`pyenv` is a tool that allows you to easily install and switch between multiple versions of Python on a single machine. It is particularly useful if you need to work with multiple projects that require different versions of Python, as it allows you to switch between the different versions without affecting the global Python installation on your system.

To install `pyenv`, you will first need to ensure that you have the necessary dependencies installed on your system.

On Linux and Mac devices use the command below:

```bash
curl https://pyenv.run | bash
```

On a Debian or Ubuntu system, you can install the dependencies with the following command:

```bash
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
```

To install `pyenv` on a Windows machine, installing with PowerShell is the easiest way to get started. Open PowerShell and run the command below:

```powershell
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"
```

If you encounter any error during installation on Windows, use the link below to find possible solutions: <https://github.com/pyenv-win/pyenv-win/blob/master/docs/installation.md#powershell>

Once the dependencies are installed, you can install `pyenv` with the following command:

```bash
curl https://pyenv.run | bash
```

This will install `pyenv` and add the necessary configuration to your shell. To start using `pyenv`, you will need to add the following lines to your shell configuration file (e.g. \~/.bashrc, \~/.zshrc, etc.):

```bash
export PATH="~/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
```

After adding these lines, you can either log out and log back in again or source your shell configuration file to apply the changes:

```bash
source ~/.bashrc
```

With `pyenv` installed, you can now use the `pyenv` command to list the available Python versions, install new versions, and switch between them. For example, to list the available Python versions, you can use the `pyenv install --list` command.

To install a specific version of Python, you can use the `pyenv` install command followed by the version number. For example, to install Python 3.9.0, you can use the following command:

```bash
pyenv install 3.9.0
```

Once you have installed multiple versions of Python using `pyenv`, you can switch between them using the `pyenv` global command. For example, to switch to Python 3.9.0, you can use the following command:

```bash
pyenv global 3.9.0
```

This will set the global Python version to 3.9.0, and any new Python environments or virtual environments that you create will use this version by default. You can also set the Python version on a per-project basis by creating a `.python-version` file in the root directory of your project, and specifying the desired Python version in the file. `pyenv` will automatically switch to the specified version whenever you enter the project directory.


# Virtual Environments

### What is a virtual environment?

A virtual environment is a separate environment in which you can install Python packages and run Python scripts without affecting the packages and scripts in the global environment. Virtual environments are useful when you have different projects that require different versions of packages, or when you want to isolate a particular project from the rest of your system.


# Pipenv

#### Setting up virtual environments using pipenv

`Pipenv` is a tool that aims to bring the best of all packaging worlds (bundler, composer, npm, cargo, yarn, etc.) to the Python world. It automatically manages project packages through the `Pipfile` file as you install or uninstall packages.

`Pipenv` is a tool that aims to simplify the process of managing package dependencies in a Python project. It does this by introducing a new file called the `Pipfile`, which contains all the package dependencies for your project. You can use the `pipenv` command to install packages, and it will automatically update the `Pipfile` and `Pipfile.lock` with the new package information. The `Pipfile.lock` file is used to ensure that the exact same packages are installed on every machine that runs `pipenv` install, which can be useful for reproducing environments and avoiding dependency conflicts.

`Pipenv` is designed to be easy to use, and it integrates with other tools such as `pyenv` to manage multiple Python versions and `pipx` to install and run Python packages in isolated environments.

## Installing `pipenv`

To install `pipenv`, you will need to have Python and pip, the Python package manager, installed on your system. If you do not have Python and `pip` already installed, you can install them by following the instructions for your operating system:

**Windows:**

Download the Python installer from the official Python website (<https://www.python.org/downloads/>) and run it to install Python on your system. Make sure to select the option to add Python to your system path during the installation.

Open a Command Prompt window and run the following command to install pip:

```bash
python -m pip install -U pip
```

**macOS:**

Python and `pip` are pre-installed on macOS. You can check if they are installed on your system by running the following commands in a terminal:

```bash
python --version
pip --version
```

**Linux:**

Python and pip are often pre-installed on Linux systems. You can check if they are installed on your system by running the following commands in a terminal:

```bash
python3 --version
pip3 --version
```

If Python and pip are not installed on your system, you can install them by running the following commands:

```bash
sudo apt-get update
sudo apt-get install python3
sudo apt-get install python3-pip
```

Once you have Python and pip installed, you can install `pipenv` by running the following command:

```bash
pip install --user pipenv
```

This will install `pipenv` in your user directory, so you will be able to use it without having to use sudo. If you want to install `pipenv` system-wide, you can run the following command instead:

```bash
sudo pip install pipenv
```

This will install `pipenv` globally, allowing it to be used by any user on the system.

You can verify that `pipenv` is installed correctly by running the following command:

```bash
pipenv --version
```

This should print the version number of `pipenv`, indicating that it is installed and working correctly.

### Creating a virtual environment using `pipenv`

Run the following command to create a new virtual environment:

```bash
pipenv --python <python_version>
```

Replace \<python\_version> with the version of Python that you want to use in the virtual environment.

For example, to create a virtual environment with Python 3.7, you would run:

```bash
pipenv --python 3.7
```

To activate the virtual environment, run the following command:

```bash
pipenv shell
```

You should now see the virtual environment's name in your terminal prompt, indicating that it is active.

To install packages in the virtual environment, you can use `pipenv` install command. For example, to install the requests package, you would run:

```bash
pipenv install requests
```

To deactivate the virtual environment, simply run the exit command i.e.

```bash
exit
```


# Virtualenv

`virtualenv` is a tool used to isolate specific Python environments on a single machine, allowing you to work with multiple versions of Python packages and libraries without conflicts. This can be useful if you need to work on multiple projects that require different versions of packages, or if you want to test your code on multiple versions of Python.

Here's how you can use `virtualenv`:

* Install `virtualenv`:

```bash
pip install virtualenv
```

* Create a new virtual environment:

```bash
virtualenv myenv
```

This will create a new directory called "myenv" that contains a copy of the Python interpreter and libraries, as well as a script called activate that you can use to activate the environment.

* Activate the virtual environment:

```bash
source myenv/bin/activate
```

This will modify your shell prompt to indicate that you are now working in the virtual environment. Your virtual environment will also be the default Python interpreter until you deactivate it.

* Install packages: With the virtual environment active, you can now install packages using pip as you normally would. These packages will be installed in the virtual environment, rather than globally on your system.

```bash
pip install <package-name>
```

* Deactivate the virtual environment: When you are finished working in the virtual environment, you can deactivate it by running the following command:

```bash
deactivate
```

This will restore the original Python interpreter and libraries, and remove the virtual environment from your shell prompt.


# Object Oriented Programming in Python

Object-oriented programming (OOP) is a programming paradigm that is based on the concept of "objects", which can contain data and code that manipulates that data. In Python, everything is an object and each object has a type, called a class.

In object-oriented programming, you define classes to represent the types of objects you want to manipulate, and you create objects that are instances of those classes. Each object has its own set of attributes (data) and behaviors (methods). For example, you might define a Dog class with attributes like name, breed, and age, and behaviors like bark() and fetch(). You could then create individual dog objects that represent specific dogs, like a dog1 object with a name attribute of "Fido" and a breed attribute of "Labrador".

In Python, you define a class using the class keyword, followed by the name of the class and a colon. The body of the class is indented and typically contains a series of methods (functions that are defined within the class). Here's an example of a simple Dog class:

```python
class Dog:
    def __init__(self, name, breed, age):
        self.name = name
        self.breed = breed
        self.age = age

    def bark(self):
        print("Woof!")

dog1 = Dog("Fido", "Labrador", 3)
print(dog1.name)  # Output: "Fido"
dog1.bark()  # Output: "Woof!"
```

In this example, the `__init__` method is a special method in Python that is called when an object is created. It initializes the attributes of the object. The `self` parameter is a reference to the current object, and is used to access the attributes and methods of the object.

The `bark()` method is a simple method that prints the string "Woof!". To call a method on an object, you use the dot notation, like object.method().

I hope this helps to give you a basic understanding of object-oriented programming in Python! Let me know if you have any questions.

#### There are some basic programming concepts in OOP:

* `Abstraction` is simplifying complex reality by modeling classes appropriate to the problem.
* `Polymorphism` is the process of using an operator or function in different ways for different data input.
* `Encapsulation` hides the implementation details of a class from other objects.
* `Inheritance` is a way to form new classes using classes that have already been defined.

At the basic level, everything in Python is an object. Objects are basic building blocks of a Python OOP (Object-oriented Programming) program.

```python
import sys

print(type(1))
print(type(""))
print(type([]))
print(type({}))
print(type(()))
print(type(object))
print(type(function))
print(type(sys))
```

In this example we show that all these entities are in fact objects. The type function returns the type of the object specified when we run the code block.

```bash
<class 'int'>
<class 'str'>
<class 'list'>
<class 'dict'>
<class 'tuple'>
<class 'type'>
<class 'function'>
<class 'module'>
```

*Integers, strings, lists, dictionaries, tuples, functions, and modules are Python objects.*


# Class

## Class keyword

In our previous example, we saw the python types were all built-in objects of the Python programming language. The user defined objects are created using the `class keyword`. The class is a blueprint that defines a nature of a future object. From classes, we construct instances. An instance is a specific object created from a particular `class`. For example, `Huck` might be an instance of a `Dog class`.

```python
class First:
    pass

fr = First()

print(type(fr))
print(type(First))
```

This is our first class. The body of the class is left empty for now. It is a convention to give classes a name that starts with a capital letter.

```python
class First:
    pass
```

Here we define the First class. Note that by default, all classes inherit from the base object.

```python
fr = First()
```

Here we see that `fr` is an instance object of the First class.

Inside a `class`, we can define attributes and methods. An attribute is a characteristic of an object. This can be for example a salary of an employee. A method defines operations that we can perform with our objects. A method might define a cancellation of an account.

Technically, attributes are variables and methods are functions defined inside a class.

#### Object initialization

A special method called `__init__` is used to initialize an object.

```python
# object_initialization.py

class Being:
    def __init__(self): # this initialises our object
        print("Being is initialized")

Being()
```

#### Python Object Attributes

Attributes are characteristics of an object. Attributes are set in the `__init__` method.

```python
class Cat:
    def __init__(self, name):
        self.name = name

missy = Cat('Missy')
lucky = Cat('Lucky')

print(missy.name)
print(lucky.name)
```

In this code example, we have a `Cat class`. The special method `__init__` is called automatically right after the object has been created.

```python
def __init__(self, name):
```

Each method in a class definition begins with a reference to the instance object. It is by convention named self. There is nothing special about the self name. We could name it this, for example. The second parameter, name, is the argument. The value is passed during the class initialization.

```python
self.name = name
```

Here we pass an attribute to an instance object.

```python
missy = Cat('Missy')
lucky = Cat('Lucky')
```

Here we create two objects: cats Missy and Lucky. The number of arguments must correspond to the `__init__` method of the class definition. The 'Missy' and 'Lucky' strings become the name parameter of the `__init__` method.

```python
print(missy.name)
print(lucky.name)
```

The attributes can be assigned dynamically, not just during initialization. This is demonstrated by the next example.

```python
class Person:
    pass

p = Person()
p.age = 24
p.name = "Peter"

print("{0} is {1} years old".format(p.name, p.age))
```

#### Python Class Attributes

So far, we have been talking about instance attributes. In Python there are also so-called class object attributes. Class object attributes are same for all instances of a class.

```python
class Cat:
    species = 'mammal' # this is a class attribute
    def __init__(self, name, age):
        self.name = name
        self.age = age

missy = Cat('Missy', 3)
lucky = Cat('Lucky', 5)

print(missy.name, missy.age)
print(lucky.name, lucky.age)

print(Cat.species)
print(missy.__class__.species)
print(lucky.__class__.species)
```

In our example, we have two cats with specific `name` and `age` attributes. Both cats share some characteristics. Missy and Lucky are both mammals. This is reflected in a class level attribute species. The attribute is defined outside any method name in the body of a class.

```python
print(Cat.species)
print(missy.__class__.species)
```

There are two ways how we can access the class object attributes: either via the name of the `Cat class`, or with the help of a special `__class__` attribute.

Executing the code block above gives us this result:

```bash
Missy 3
Lucky 5
mammal
mammal
mammal
```


# Class Methods

Methods are functions defined inside the body of a class. They are performed operations with the attributes of our objects.

Methods are essential in the encapsulation concept of the OOP paradigm. For example, we might have a connect method in our `ConnectDatabase` class. We need not be informed how exactly the method connects to the database. We only know that it is used to connect to a database. This is essential in dividing responsibilities in programming, especially in large applications.

```python
# methods.py

class Circle:
    pi = 3.141592
    def __init__(self, radius=1):
        self.radius = radius

    def area(self):
        return self.radius * self.radius * Circle.pi

    def setRadius(self, radius):
        self.radius = radius

    def getRadius(self):
        return self.radius

c = Circle()

c.setRadius(5)
print(c.getRadius())
print(c.area())
```

In the code example, we have a `Circle class`. We define three new methods.

```python
def area(self):
    return self.radius * self.radius * Circle.pi
```

The area method returns the area of a circle.

```python
def setRadius(self, radius):
    self.radius = radius
```

The `setRadius` method sets a new value for the radius attribute.

```python
def getRadius(self):
    return self.radius
```

The `getRadius` method returns the current radius.

```python
c.setRadius(5)
```

The method is called on an instance object. The `c` object is paired with the self parameter of the class definition. The number `5` is paired with the radius parameter.

```bash
$ ./methods.py 

5
78.5398
```

In Python, we can call methods in two ways. There are bounded and unbounded method calls.

```python
# bound_unbound_methods.py

class Methods:
    def __init__(self):
        self.name = 'Methods'

    def getName(self):
        return self.name

m = Methods()

print(m.getName())
print(Methods.getName(m))
```

In this example, we demonstrate both method calls.

```python
print(m.getName())
```

This is the bounded method call. The Python interpreter automatically pairs the `m` instance with the self parameter.

```python
print(Methods.getName(m))
```

And this is the unbounded method call. The instance object is explicitly given to the `getName` method.

```bash
$ ./bound_unbound_methods.py 
Methods
Methods
```


# Python Class Inheritance

Inheritance is a way to form new classes using classes that have already been defined. The newly formed classes are called derived classes, the classes that we derive from are called base classes. Important benefits of inheritance are code reuse and reduction of complexity of a program. The derived classes (descendants) override or extend the functionality of base classes (ancestors).

```python
class Animal:

    def __init__(self):
        print("Animal created")

    def whoAmI(self):
        print("Animal")

    def eat(self):
        print("Eating")


class Dog(Animal):

    def __init__(self):
        super().__init__()
        
        print("Dog created")

    def whoAmI(self):
        print("Dog")

    def bark(self):
        print("Woof!")

d = Dog()
d.whoAmI()
d.eat()
d.bark()
```

In this example, we have two classes: `Animal` and `Dog`. The `Animal` is the base class, the `Dog` is the derived class. The derived class inherits the functionality of the base class. It is shown by the eat method. The derived class modifies existing behavior of the base class, shown by the `whoAmI` method. Finally, the derived class extends the functionality of the base class, by defining a new bark method.

```python
class Dog(Animal):
    def __init__(self):
        super().__init__()
        print("Dog created")
```

We put the ancestor classes in round brackets after the name of the descendant class. If the derived class provides its own `__init__` method, and we want to call the parent constructor, we have to explicitly call the base class `__init__` method with the help of the super function.

```bash
$ ./inherit.py 

Animal created
Dog created
Dog
Eating
Woof!
```


# Python Class Polymorphism

Polymorphism is the process of using an operator or function in different ways for different data input. In practical terms, polymorphism means that if `class B` inherits from `class A`, it doesn't have to inherit everything about class A; it can do some of the things that class A does differently.

```python
# basic_polymorphism.py

a = "alfa"
b = (1, 2, 3, 4)
c = ['o', 'm', 'e', 'g', 'a']

print(a[2])
print(b[1])
print(c[3])
```

Python uses polymorphism extensively in built-in types. Here we use the same indexing operator for three different data types.

Polymorphism is mostly used when dealing with inheritance.

```python
# polymorphism.py

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

   def talk(self):
      pass

class Cat(Animal):
   def talk(self):
      print("Meow!")

class Dog(Animal):
   def talk(self):
      print("Woof!")

a = Animal()
a.talk()

c = Cat("Missy")
c.talk()

d = Dog("Rocky")
d.talk()
```

Here we have two species: a *dog* and a *cat*. Both are *animals*. The `Dog` class and the `Cat` class inherit the `Animal` class. They have a talk method, which gives different output for them.

```bash
$ ./polymorphism.py 

Meow!
Woof!
```


# Python special methods

In Python, special methods (also known as dunder methods) are a set of predefined methods that you can use to enrich your classes. They are also known as "magic methods" because they begin and end with double underscores (e.g. `__init__`).

These methods allow you to define the behavior of certain operations (such as addition, comparison, etc.) that are applied to your objects. For example, you can use the `__len__` method to define the behavior of the len() function when it is called on an instance of your class.

Here is a list of some common special methods in Python:

**Initialization and Construction Description**

* `__new__(cls, other)` To get called in an object's instantiation.
* `__init__(self, other)` To get called by the `__new__` method.
* `__del__(self)` Destructor method.

**Unary operators and functions Description**

* `__pos__(self)` To get called for unary positive e.g. +someobject.
* `__neg__(self)` To get called for unary negative e.g. -someobject.
* `__abs__(self)` To get called by built-in `abs()` function.
* `__invert__(self)` To get called for inversion using the \~ operator.
* `__round__(self, n)` To get called by built-in `round()` function.
* `__floor__(self)` To get called by built-in `math.floor()` function.
* `__ceil__(self)` To get called by built-in `math.ceil()` function.
* `__trunc__(self)` To get called by built-in `math.trunc()` function.

**Augmented Assignment Description**

* `__iadd__(self, other)` To get called on addition with assignment e.g. a +=b.
* `__isub__(self, other)` To get called on subtraction with assignment e.g. a -=b.
* `__imul__(self, other)` To get called on multiplication with assignment e.g. a \*=b.
* `__ifloordiv__(self, other)` To get called on integer division with assignment e.g. a //=b.
* `__idiv__(self, other)` To get called on division with assignment e.g. a /=b.
* `__itruediv__(self, other)` To get called on true division with assignment
* `__imod__(self, other)` To get called on modulo with assignment e.g. a%=b.
* `__ipow__(self, other)` To get called on exponents with assignment e.g. a \*\*=b.
* `__ilshift__(self, other)` To get called on left bitwise shift with assignment e.g. a<<=b.
* `__irshift__(self, other)` To get called on right bitwise shift with assignment e.g. a >>=b.
* `__iand__(self, other)` To get called on bitwise AND with assignment e.g. a&=b.
* `__ior__(self, other)` To get called on bitwise OR with assignment e.g. a|=b.
* `__ixor__(self, other)` To get called on bitwise XOR with assignment e.g. a ^=b.

**Type Conversion Magic Methods Description**

* `__int__(self)` To get called by built-int `int()` method to convert a type to an int.
* `__float__(self)` To get called by built-int `float()` method to convert a type to float.
* `__complex__(self)` To get called by built-int `complex()` method to convert a type to complex.
* `__oct__(self)` To get called by built-int `oct()` method to convert a type to octal.
* `__hex__(self)` To get called by built-int `hex()` method to convert a type to hexadecimal.
* `__index__(self)` To get called on type conversion to an int when the object is used in a slice expression.
* `__trunc__(self)` To get called from `math.trunc()` method.

**String Magic Methods Description**

* `__str__(self)` To get called by built-int `str()` method to return a string representation of a type.
* `__repr__(self)` To get called by built-int `repr()` method to return a machine readable representation of a type.
* `__unicode__(self)` To get called by built-int `unicode()` method to return an unicode string of a type.
* `__format__(self, formatstr)` To get called by built-int `string.format()` method to return a new style of string.
* `__hash__(self)` To get called by built-int `hash()` method to return an integer.
* `__nonzero__(self)` To get called by built-int `bool()` method to return True or False.
* `__dir__(self)` To get called by built-int `dir()` method to return a list of attributes of a class.
* `__sizeof__(self)` To get called by built-int `sys.getsizeof()` method to return the size of an object.

**Attribute Magic Methods Description**

* `__getattr__(self, name)` Is called when the accessing attribute of a class that does not exist.
* `__setattr__(self, name, value)` Is called when assigning a value to the attribute of a class.
* `__delattr__(self, name)` Is called when deleting an attribute of a class.

**Operator Magic Methods Description**

* `__add__(self, other)` To get called on add operation using + operator
* `__sub__(self, other)` To get called on subtraction operation using - operator.
* `__mul__(self, other)` To get called on multiplication operation using \* operator.
* `__floordiv__(self, other)` To get called on floor division operation using // operator.
* `__truediv__(self, other)` To get called on division operation using / operator.
* `__mod__(self, other)` To get called on modulo operation using % operator.
* `__pow__(self, other[, modulo])` To get called on calculating the power using \*\* operator.
* `__lt__(self, other)` To get called on comparison using < operator.
* `__le__(self, other)` To get called on comparison using <= operator.
* `__eq__(self, other)` To get called on comparison using == operator.
* `__ne__(self, other)` To get called on comparison using != operator.
* `__ge__(self, other)` To get called on comparison using >= operator.

There are many other special methods in Python, and you can find a complete list in the [documentation](https://docs.python.org/3/reference/datamodel.html#special-method-names).

```python
# special_methods.py

class Book:
    def __init__(self, title, author, pages):
         """
         __init__ method. The method creates a new instance of a Book class.
         """
        print("A book is created")
        self.title = title
        self.author = author
        self.pages = pages

    def __str__(self):
         """
          __str__ method. This method should return an informal string representation of an object.
         """
        return "Title:{0} , author:{1}, pages:{2} ".format(
            self.title, self.author, self.pages)

    def __len__(self):
         """
          __len__ method. In our case, we print the number of pages of our book.
         """
        return self.pages

    def __del__(self):
         """
          The del keyword deletes an object. It invokes its __del__ method.
         """
        print("A book is destroyed")

book = Book("Inside Steve's Brain", "Leander Kahney", 304)

print(book)
print(len(book))
del book
```


# Appendix

[Python Documentation](https://docs.python.org/3/contents.html)


# Contributing

We'd love your help! Thanks for caring about the book.

### Where to Edit

All edits should be made in the `src` directory.

### Checking for Fixes

### Licensing

### Code of Conduct

### Expectations

### Help wanted

### Translations


