# Fundamentals of Python

December 23, 2025 Python Fundamentals

There is no doubt that python is currently one of the most used languages in the world. It is a well crafted language with great libraries and is used in a wide range of domains. Being able to write python is a useful skill for any developer to have.

There is no doubt that python is currently one of the most used languages in the world. It is a well crafted language with great libraries and is used in a wide range of domains. Being able to write python is a useful skill for any developer to have.

The essentials

Printing to console

# print message with new line at the end
print("Hello world")

# print formatted message
print("Hello %s, are you %d years old?" % ("User", 20))

# print without new line at the end
print("Hello there", end="")

Comments

# This is a single line comment

"""
      This is a multiline comment
      Also known as comment-block
"""

Operators

TypeOperatorExplanation
Arithmetic operators()Precedence 1st
**Precedence 2nd
% / // *Precedence 3rd
+ -Precedence 4th
Compound operators+=
-=
*=*
/=
**=Power and equal
%=Mod and equal
//=Floor divisor and equal
Logical operatorsnotPrecedence 1st
andPrecedence 2nd
orPrecedence 3rd

Match

# basic example
def run_command(command: str) -> None:
    match command:
        case "reset":
            print("resetting system")
        case "exit":
            print("exiting...")
            exit()
        case other:
            print(f"unrecognized command: '{command}'")


def main() -> None:
    while True:
        command = input("$ ")
        run_command(command)
# more advanced usages
def run_command(command: str) -> None:
    match command.split():
        case ["load" | "open", filename]:
            print(f"opening file: {filename}")
        case ["save"]:
            print("saving current file")
        case ["quit" | "exit", *rest] if "--force" in rest or "-f" in rest:
            print("force exiting...")
            exit()
        case ["quit" | "exit"]:
            print("exiting...")
            exit()
        case _:
            print(f"unrecognized command: '{command}'")


def main() -> None:
    while True:
        command = input("$ ")
        run_command(command)

String

message: str = "Hello world"

# access a character using subscript
alphabet: str = message[0]

# access first five characters
word: str = message[:5]

# access last five words
last_word: str = message[6:11]

# length of string
length: int = len(message)

# convert number to string
num_string: str = str(10)

String methods

OperatorDescription
title()Capitalize the first letter of each word
upper()All letters are uppercase
lower()All letters are lowercase
strip()Remove whitespace from start and end of the string
lstrip()Remove whitespace from the start of the string
rstrip()Remove whitespace from the end of the string
split()Split string into a list depending on the delimiter
zfill()Adds zeros at the beginning of the string, until it reaches the specified length (provided as argument).

Fstring - String interpolation

name: str = "Mr. Client"
balance: float = 3000.5

message: str = f"Hello {name}, your current balance is USD {balance:.2f}"
print(message)

# Hello Mr. Client, your current balance is USD 3000.50

Formatting rules

OperatorDescription
%sStrings
%dIntegers
%fFloats
%.3fFloating point number with 3 decimal points
%%Print out % symbol

List

nums: list[int] = [1, 2, 3, 4, 5]

# add element to end of list
nums.append(6)

# insert element at start of list (i.e. index 0)
nums.insert(0, 10)

# remove item from list
nums.remove(5)

# access element at index
result = nums[1]

# apply operation to all elements in list
mapped = map(lambda n: n * 2, nums)
print(list(mapped))

# filter out elements based on criteria
filtered = filter(lambda n: n % 2 == 0, nums)
print(list(filtered))

# sort a list - original is modified
nums.sort(reverse=False)
print(nums)

# loop through all elements in list (with index)
for i, v in enumerate(nums):
    print(i, v)

# check if element does not exist in list
if 5 not in nums:
    print("5 does not exist inside list")
# list slices
nums: list[int] = [1, 2, 3, 4, 5]
slice: list[int] = nums[1:3]
print(slice)
# list comprehensions
nums: list[int] = [n for n in range(1, 5)]
print(nums)                                 # [1, 2, 3, 4]

nums = [(n+1)*10 for n in range(1, 5)]
print(nums)                                 # [20, 30, 40, 50]

base: list[int] = [10, 21, 32, 44, 55]
nums = [n for n in base if n % 2 == 0]
print(nums)                                 # [10, 32, 44]

Tuple

Tuples are an immutable collection of elements

values: tuple[int, str, bool] = (300, "Someone", True,)
print(values)

# tuple comprehensions
nums: tuple[int, ...] = tuple(n for n in range(1, 4))
print(nums)  # (1, 2, 3)

Sets

Sets is a list of unique values. Values inside sets don’t have index values. Sets cannot be sliced.

numbers: set[int] = {10, 20, 30, 20}
print(numbers)          # {10, 20, 30}

# add an element to set, order is not maintained
numbers.add(50)
print(numbers)          # {10, 50, 20, 30}

# remove an element from set
numbers.remove(10)
print(numbers)          # { 50, 20, 30}

# generate set using comprehensions
nums_set: set[int] = {n for n in range(1, 10)}
print(nums_set)

Dictionary

A dictionary is like a list but unlike lists dictionaries are used to store Key-Value Pairs.

capitals: dict[str, str] = {
    "Pakistan": "Islamabad",
    "China": "Beijing",
    "Japan": "Tokyo"
}

# read a value
value: str | None = capitals.get("China")
if value:
    print(value)

# add new values
capitals.update({"England": "London"})

# remove an item
del capitals["China"]

# check if value exists in dict
if "Japan" in capitals:
    print("Japan exists in records")

# loop over dict key and value
for k, v in capitals.items():
    print(k, v)
# Dictionary comprehensions
names: list[str] = ["Stark", "Banner", "Steve", "Romanoff"]
heroes: list[str] = ["Iron Man", "Hulk", "Captain America", "Black Widow"]

identities: dict[str, str] = {
    name: hero for name, hero in zip(names, heroes)
}

print(identities)
# {'Stark': 'Iron Man', 'Banner': 'Hulk', 'Steve': 'Captain America', 'Romanoff': 'Black Widow'}
# Pass (and spread) dictionary to function
def greet_user(name: str, email: str) -> None:
    print("Hello %s, you have registered with email %s" % (name, email))


def main() -> None:
    args: dict[str, str] = {
        "name": "someone",
        "email": "someone@site.com",
    }
    greet_user(**args)

Counter

Counter is a built-in class which can help keep track of item occurrences in lists.

from collections import Counter

c = Counter(["Python", "PHP"])
c.update(["Go", "JS"])
c.update(["PHP", "JS", "TS"])

print(c)
# Counter({'PHP': 2, 'JS': 2, 'Python': 1, 'Go': 1, 'TS': 1})

print(c.most_common(3))
# [('PHP', 2), ('JS', 2), ('Python', 1)]

Taking Input from User

# interactive input
name: str = input("Please enter your name: ")
import sys

# command-line arguments
for arg in sys.argv:
	    print(arg)

Functions

# accept an unknown number of arguments
def variadic_func(*args: str) -> None:
    for arg in args:
        print(arg)


# program entry-point function
def main() -> None:
    nums: list[int] = [n for n in range(10)]
    print(nums)
    variadic_func("hello", "world")


# wrap entry-point function for error handling
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("gracefully shutting down...")
    except Exception as err:
        print("error: ", err)
# pass by value
def increment_num(num: int) -> None:
    num += 1


def main() -> None:
    num: int = 300
    increment_num(num)
    print(num)

# 300
# pass by reference
def add_item(items: list[int], new_item: int) -> None:
    items.append(new_item)


def main() -> None:
    nums: list[int] = [i for i in range(1, 10)]
    add_item(nums, 300)
    print(nums)

# [1, 2, 3, 4, 5, 6, 7, 8, 9, 300]
# lambdas
square = lambda n: n**2

# X is returned if X is larger than y, otherwise y is returned
greater = lambda x, y = x if x > y else y
# Higher Order functions
from typing import Callable


# apply callback function to all values in the List
def map(values: list[int], callback: Callable[[int], int]) -> list[int]:
    return [callback(v) for v in values]


def main() -> None:
    nums: list[int] = [n for n in range(10)]
    new_nums: list[int] = map(nums, lambda n: n * 2)
# closures
from typing import Callable


def generate_tag(html_tag: str) -> Callable[[str], None]:
    # free variable
    tag = html_tag

    def inline_msg(msg: str) -> None:
        html_code = f"<{tag}>{msg}</{tag}>"
        print(html_code)

    # return the inner function
    return inline_msg


def main() -> None:
    # run the outer function and assign inner function to variable (i.e. heading_1)
    heading_1 = generate_tag("h1")

    # run the inner function
    heading_1("This is a heading")
    heading_1("This is another heading")
# generators
from typing import Iterator


# produce a generator
def list_generator(start: int, end: int) -> Iterator[int]:
    for element in range(start, end):
        yield element**2


def main() -> None:
    results = list_generator(1, 10)

    # prints reference of generator
    print(results)

    # prints first value: 1
    print(next(results))

    # loops over all remaining values
    for i in results:
        print(i)
# Iterators
from __future__ import annotations


class RangeIterator:
    i: int = 0
    limit: int = 0

    def __init__(self, limit: int):
        self.limit = limit

    def __iter__(self) -> RangeIterator:
        return self

    def __next__(self) -> int:
        self.i += 1
        if self.i >= self.limit:
            raise StopIteration

        return self.i


def main() -> None:
    for i in RangeIterator(10):
        print(i)

Note: Above code can be made even better in Python 3.11 by using the Self type.

from typing import Self


class RangeIterator:
    i: int = 0
    limit: int = 0

    def __init__(self, limit: int):
        self.limit = limit

    def __iter__(self) -> Self:
        return self

    def __next__(self) -> int:
        self.i += 1
        if self.i >= self.limit:
            raise StopIteration

        return self.i

Exception Handling

def main() -> None:
    entry: str = input("Enter Pin: ")
    pin: int = 12345

    if int(entry) == pin:
        print("correct pin")
    else:
        print("incorrect pin")


if __name__ == "__main__":
    try:
        main()
    except ValueError:
        print("error: pin cannon contain non-numeric characters")
    except KeyboardInterrupt:
        print("ctrl+c: closing program...")
    except Exception as err:
        print("error: ", err)

File Handling

def main() -> None:
    path: str = "./sample.txt"


    # use context manager: file will be automatically closed
    with open(path, "r") as file:
        # read entire content into memory
        content: str = file.read()
        print(content)

File modes

OperatorDescription
rRead-only
wWrite mode. If the file doesn’t exist, create it. If the file already exists, overwrite it
aAppend mode, write to the end of the file. Never overwrite.
r+Open file in read and write mode
tText mode
bBinary mode
# read file, line-by-line
def main() -> None:
    path: str = "./sample.txt"

    with open(path, "r") as file:
        for line in file:
            print(line)
# reading files in chunks
def main() -> None:
    path: str = "./sample.txt"
    chunk_size: int = 64

    with open(path, "r") as file:
        content: str = file.read(chunk_size)

        while len(content) > 0:
            print(content)
            content = file.read(chunk_size)

Writing to files

def main() -> None:
    path: str = "./sample.txt"

    with open(path, "w") as file:
        file.write("This is line one\n")
        file.write("This is line two\n")
# copy binary files
def main() -> None:
    input_file_path: str = "./input.jpg"
    output_file_path: str = "./output.jpg"

    with open(input_file_path, "rb") as read_file:
        with open(output_file_path, "wb") as write_file:
            for line in read_file:
                write_file.write(line)

Context Managers

Context managers help manage resources effectively. Even in case of errors, they ensure that the resource is disconnected properly.

Define using Classes

class open_file:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    # context setUp
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    # context tearDown
    def __exit__(self, exc_type, exc_val, traceback):
        self.file.close()


def main() -> None:
    # using the context manager
    with open_file("./sample.txt", "a") as file:
        file.write("Sample text\n")
from contextlib import contextmanager


@contextmanager
def open_file(filename: str, mode: str):
    try:
        # init
        file = open(filename, mode)
        # enter i.e. setUp
        yield file

    finally:
        # exit i.e. tearDown
        file.close()


def main() -> None:
    with open_file("./sample.txt", "a") as file:
        file.write("Some sample text\n")

Environment variables

import os


def main() -> None:
    # read an environment variable
    home: str | None = os.getenv("HOME")
    print(home)

    # list all env variables, environ is dict[str, str]
    for k, v in os.environ.items():
        print(k, v)

Note: For loading .env files, please look at the package dotenv.

Filesystem

import os
from os import path

# get current directory
pwd: str = os.getcwd()

# create path
current_path: str = path.join(pwd, ".gitignore")

# change current directory
new_path: str = path.join(pwd, ".git")
os.chdir(new_path)

# list all items in directory
content: list[str] = os.listdir()

# get basename (i.e. filename) from path
filename: str = path.basename(current_path)

# get directory name from path
directory: str = path.dirname(current_path)

# check if path exists
path_exits: bool = path.exists(current_path)

# check if path is file
is_path_file: bool = path.isfile(current_path)

# check if path is a directory
is_path_dir: bool = path.isdir(current_path)

# create a new folder
os.mkdir("sample")

# create a folder few levels deep
os.makedirs("./New/Folder/NEW")

# remove a directory
os.rmdir("New Folder")

# remove directories few levels deep
os.removedirs("./New/Folder/NEW")

# rename files and Folders
os.rename("NEW", "New-Folder")

JSON

MethodDescription
json.loads(var)Load string as JSON
json.dumps(var)Encode and return JSON string
json.load(file_obj)Read JSON file from file. Open file nees to be provided
json.dump(data_obj, file_obj)Write JSON to provided file

Random

import random

# random number between 0 and 1
float_num: float = random.random()

# random float between provided range, end is non-inclusive
float_num = random.uniform(10, 20)

# random int, end is non-inclusive
int_num: int = random.randint(10, 20)

# pick a random value from list
choices: list[str] = ["Pakistan", "China", "Canada"]
random_choice: str = random.choice(choices)

# random shuffle elements in list, updates original list
even_nums: list[int] = [n for n in range(10, 30) if n % 2 == 0]
random.shuffle(even_nums)

# grab random sample from a list, k is number of items to grab
sample_items: list[int] = random.sample(even_nums, k=3)

Dates

import datetime

# create date instance
future_date = datetime.date(2023, 4, 21)

# get current date
today = datetime.date.today()

# get components of date
print("%s, %s, %s" % (today.year, today.month, today.day))

# get date after n days
week_delta = datetime.timedelta(days=7)

# date next week
next_week = today + week_delta

Track time of execution

from typing import Callable
import datetime


# track execution time decorator
def time_execution(func: Callable[[], None]) -> Callable[[], None]:
    def wrapper() -> None:
        timer_start = datetime.datetime.now()
        func()
        timer_end = datetime.datetime.now()
        elapsed = timer_end - timer_start
        print("elapsed: %s ms" % (elapsed.microseconds, ))

    return wrapper


@time_execution
def slow_task() -> None:
    for i in range(1, 1000000):
        pass


def main() -> None:
    slow_task()

Time

time = datetime.time(22, 10, 50, 1000 )	# hours, minutes, seconds, milliseconds
print(time)
# output: 22:10:50.001000

# print current time
time = datetime.datetime.now()
print(f"{time: %H:%M:%S}")

Date-time

time = datetime.datetime(2016, 9, 13, 22, 10, 50, 1000 )
# year, month, date, hour, minutes, seconds, milliseconds
print(time)
print(time.date())		# 2016-09-13
print(time.time())		# 22:10:50.001000

Parse Arguments

import argparse
from dataclasses import dataclass


@dataclass
class Args:
    name: str
    year: int


def main(args: Args) -> None:
    print(f"Hello {args.name}, you were born in {args.year}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Program description")
    parser.add_argument("-n", "--name", type=str, help="username", default="User")
    parser.add_argument("-y", "--year", type=int, help="year of birth")

    parsed = parser.parse_args()
    args = Args(parsed.name, parsed.year)
    main(args)