Python
June 14, 2026

Learning Python Foundations with Practical Examples for New Developers

Python becomes much easier when you stop treating it as a collection of random syntax rules and start seeing it as a small set of building blocks that work together. Those building blocks are variables, objects, data types, data structures, control flow, functions, modules, and formatting habits.

This tutorial teaches those foundations in a practical way. The goal is not to memorize every detail of the language. The goal is to understand enough Python to read code confidently, write small scripts, organize reusable logic, and prepare yourself for data analysis work with libraries such as pandas.

The examples use simple business-style data, such as orders, users, currencies, dates, and file names. That makes the concepts easier to connect to real automation and data-cleaning tasks.

What You Need to Understand First

A Python program is usually made from small instructions that transform input into output.

Input data
  |
  v
Variables and data structures
  |
  v
Conditions, loops, and functions
  |
  v
Clean output, files, reports, or further analysis

For a beginner, the most important question is not "How many Python features do I know?" The better question is "Can I represent data clearly and process it safely?"

Python helps with this because the language is consistent:

  • The same assignment syntax works for numbers, text, lists, dictionaries, and custom objects.
  • Indentation is part of the language, so code structure is visible.
  • Built-in data structures are powerful enough for many scripts.
  • Functions and modules let you move from quick experiments to maintainable code.

Variables, Objects, Attributes, and Methods

In Python, almost everything you work with is an object. A number is an object, a string is an object, a list is an object, and a function is also an object.

A variable is a name that points to an object. You create a variable with =.

invoice_count = 12
average_amount = 249.75
customer_name = "Mira"

print(invoice_count)
print(average_amount)
print(customer_name)

Python uses dynamic typing, which means a variable can later point to a different kind of object.

status = "pending"
print(status)

status = True
print(status)

That flexibility is useful, but it also means you should choose clear variable names. A name like status can be ambiguous if it sometimes stores text and sometimes stores a boolean value. A better version would be:

status_text = "pending"
is_approved = True

Objects can expose attributes and methods.

  • An attribute stores information about an object.
  • A method performs an action on an object.

You access both with dot notation.

message = "python for data work"

print(message.upper())
print(message.title())

Here, upper() and title() are methods on the string object. You call a method with parentheses. If an object has a plain attribute, you read it without parentheses.

Core Data Types

Python handles different kinds of values with different data types. The basic ones you will use constantly are:

Type Meaning Example
int Whole number 42
float Decimal number 42.5
bool True or false value True
str Text "hello"
None No value None

You can inspect a value with type().

print(type(25))
print(type(25.0))
print(type("25"))
print(type(False))

A common beginner mistake is to assume that 25, 25.0, and "25" are interchangeable. They are not.

quantity = 25
unit_price = 3.5
quantity_as_text = "25"

print(quantity * unit_price)
print(quantity_as_text * 2)

The first multiplication calculates a numeric result. The second repeats a string. Python is doing exactly what you asked, but the result can surprise you if you are not thinking about data types.

Integers and Floats

Use int for whole numbers and float for decimal numbers.

items = 7
price = 19.95

subtotal = items * price
print(subtotal)

You can convert between them when needed.

raw_year = 2026.0
clean_year = int(raw_year)

print(clean_year)
print(type(clean_year))

When converting a float with decimals to an integer, Python removes the fractional part.

print(int(8.9))

That is not rounding. It is truncation. If you need a rounded value, do not use int() as a rounding function.

Floating-Point Precision

Computers store many decimal numbers in binary form. Some decimal values cannot be represented exactly. Because of that, decimal arithmetic can show tiny differences.

result = 1.125 - 1.1
print(result)

For most everyday data-analysis calculations, this is acceptable. The important part is to know that floating-point values are approximations. Do not write fragile code that expects every decimal calculation to be visually perfect.

Booleans

A boolean is either True or False.

has_discount = True
is_archived = False

print(has_discount)
print(is_archived)

Python uses comparison operators to produce boolean values.

amount = 150

print(amount == 150)
print(amount != 200)
print(amount > 100)
print(amount <= 150)

Use and, or, and not to combine boolean logic.

customer_is_active = True
invoice_is_paid = False

print(customer_is_active and invoice_is_paid)
print(customer_is_active or invoice_is_paid)
print(not invoice_is_paid)

Python also lets you chain comparisons in a readable way.

score = 82
print(70 <= score <= 100)

None

None represents the absence of a value. It is useful when a value is optional, unknown, or not available yet.

approval_date = None

if approval_date is None:
    print("The order has not been approved yet.")

Many beginners use empty strings such as "" for missing values. That can work, but None is often clearer when the value is truly missing rather than just empty text.

Working with Strings

A string is text. You can create strings with single quotes or double quotes.

product = "Keyboard"
category = 'Accessories'

print(product)
print(category)

The quote style is mostly a readability choice. Pick the style that avoids escaping.

note = "Don't cancel the shipment."
quote = 'The customer wrote: "Please deliver after lunch."'

print(note)
print(quote)

If you need to include special characters, you can escape them with a backslash.

path_hint = "Use C:\\data\\reports for local files."
print(path_hint)

Joining Text with f-strings

An f-string lets you combine variables and text cleanly. Put f before the string and place variables inside curly braces.

customer = "Lea"
order_total = 438.5

message = f"Customer {customer} has an order total of {order_total}."
print(message)

This is easier to read than manually joining many text fragments.

customer = "Lea"
order_total = 438.5

message = "Customer " + customer + " has an order total of " + str(order_total) + "."
print(message)

Both examples work, but f-strings are usually clearer.

Useful String Methods

String methods are helpful when cleaning user input.

raw_country = "  sweden  "

clean_country = raw_country.strip().title()
print(clean_country)

You can also normalize text before comparing it.

answer = "YES"

if answer.lower() == "yes":
    print("Confirmed")

This avoids bugs caused by uppercase and lowercase differences.

Indexing and Slicing

Many Python objects are sequences. A sequence is an ordered collection of items. Strings are sequences of characters. Lists and tuples are also sequences.

Python uses zero-based indexing. The first item is at index 0, not index 1.

code = "PYTHON"

print(code[0])
print(code[1])
print(code[-1])
print(code[-2])

Negative indexes count from the end. -1 means the last item.

Slicing a Sequence

Slicing extracts part of a sequence.

reference = "INV20260415"

prefix = reference[:3]
year = reference[3:7]
month = reference[7:9]
day = reference[9:]

print(prefix)
print(year)
print(month)
print(day)

The syntax is:

sequence[start:stop:step]

The start position is included. The stop position is excluded.

letters = "ABCDEFG"

print(letters[0:3])
print(letters[2:5])
print(letters[::2])
print(letters[::-1])

Slicing is especially useful when dealing with fixed-format identifiers, dates, currency pairs, file names, or codes from external systems.

Lists: Ordered Collections You Can Change

A list stores multiple values in order. Lists are mutable, which means you can change them after creating them.

files = ["sales_jan.xlsx", "sales_feb.xlsx", "sales_mar.xlsx"]

print(files[0])
print(files[-1])

You can add items.

files = ["sales_jan.xlsx", "sales_feb.xlsx"]

files.append("sales_mar.xlsx")
files.insert(0, "sales_dec.xlsx")

print(files)

You can remove items.

files = ["draft.xlsx", "final.xlsx", "archive.xlsx"]

last_file = files.pop()
del files[0]

print(last_file)
print(files)

Use len() to count items and in to check membership.

regions = ["North", "South", "West"]

print(len(regions))
print("South" in regions)
print("East" in regions)

Nested Lists

A nested list is a list that contains other lists. This can represent a small table.

sales_rows = [
    ["Jan", 1200, 310],
    ["Feb", 980, 275],
    ["Mar", 1430, 360],
]

print(sales_rows[0])
print(sales_rows[1][2])

Nested lists are useful for simple examples, but they become hard to manage when data grows. For serious tabular data, pandas DataFrames are usually a better choice. Still, understanding nested lists helps you understand how table-shaped data can be represented in Python.

Dictionaries: Data by Name Instead of Position

A dictionary maps keys to values. Use a dictionary when each value has a meaningful name.

invoice = {
    "number": "INV-1007",
    "customer": "Nora",
    "amount": 349.9,
    "paid": False,
}

print(invoice["customer"])
print(invoice["amount"])

Lists are accessed by position. Dictionaries are accessed by key.

This makes dictionaries excellent for structured records.

invoice["paid"] = True
invoice["currency"] = "SEK"

print(invoice)

If a key might not exist, use get() with a fallback value.

invoice = {
    "number": "INV-1007",
    "amount": 349.9,
}

print(invoice.get("customer", "Unknown customer"))

Without get(), trying to read a missing key raises an error.

A List of Dictionaries

A list of dictionaries is a common structure for small datasets.

orders = [
    {"id": "A100", "country": "Sweden", "amount": 320.0, "paid": True},
    {"id": "A101", "country": "Norway", "amount": 180.0, "paid": False},
    {"id": "A102", "country": "Sweden", "amount": 510.0, "paid": True},
]

print(orders[0]["country"])

This structure is easy to loop over, filter, and transform.

Tuples: Ordered Collections That Should Not Change

A tuple is similar to a list, but it is immutable. After creating it, you cannot change its items.

supported_currencies = ("SEK", "EUR", "USD")

print(supported_currencies[0])

Use tuples when the collection is meant to stay fixed.

coordinate = (57.7, 11.9)
latitude = coordinate[0]
longitude = coordinate[1]

print(latitude)
print(longitude)

A tuple is a good choice for small, stable groups of values. A list is better when you need to add, remove, or reorder items.

Sets: Unique Values

A set stores unique values. It is useful when duplicates do not matter or when you want to compare groups.

countries = {"Sweden", "Norway", "Sweden", "Germany"}
print(countries)

The duplicate value appears only once.

You can also convert a list into a set to find unique values.

country_entries = ["Sweden", "Norway", "Sweden", "Germany", "Norway"]
unique_countries = set(country_entries)

print(unique_countries)

Sets support operations such as union and intersection.

backend_skills = {"Python", "SQL", "Docker"}
data_skills = {"Python", "Excel", "SQL"}

print(backend_skills.union(data_skills))
print(backend_skills.intersection(data_skills))

Use a set when you care about membership and uniqueness, not order.

Choosing the Right Data Structure

Need Good choice Why
Ordered items that can change list Add, remove, sort, and slice items
Named fields dict Read values by key instead of position
Fixed ordered values tuple Communicates that values should stay stable
Unique values set Removes duplicates and supports set operations

Choosing the right structure makes your code easier to read. It also prevents many small bugs.

Control Flow: Making Decisions

Control flow means deciding which code runs and how often it runs.

The if statement runs code only when a condition is true.

amount = 750

if amount >= 1000:
    print("Large order")
elif amount >= 500:
    print("Medium order")
else:
    print("Small order")

Python uses indentation to define blocks. The indented lines belong to the condition above them.

if condition:
    this line belongs to the if block
    this line also belongs to the if block
this line is outside the if block

This is not just formatting. In Python, indentation is part of the syntax.

Truthy and Falsy Values

Python objects can behave like true or false values in conditions.

Empty collections and empty strings are considered false. Non-empty ones are considered true.

selected_files = []

if selected_files:
    print("Files selected")
else:
    print("No files selected")

This is cleaner than checking len(selected_files) > 0 in many cases.

Loops: Repeating Work Safely

A for loop runs once for each item in a sequence.

countries = ["Sweden", "Norway", "Germany"]

for country in countries:
    print(country.upper())

Use meaningful loop variable names. country is better than x because it explains what the loop is processing.

Looping with a Counter

Use range() when you need numbers.

for attempt in range(3):
    print(f"Attempt number {attempt + 1}")

range(3) produces 0, 1, and 2. The stop value is not included.

Use enumerate() when you need both the index and the item.

names = ["Anna", "Omar", "Lina"]

for position, name in enumerate(names):
    print(position, name)

Looping Through Dictionaries

Looping directly over a dictionary gives you the keys.

rates = {
    "EURSEK": 11.2,
    "USDSEK": 10.4,
    "NOKSEK": 0.98,
}

for pair in rates:
    print(pair)

Use .items() when you need both the key and value.

for pair, rate in rates.items():
    print(f"{pair}: {rate}")

break and continue

Use break to stop a loop early.

amounts = [120, 340, 999, 1500, 80]

for amount in amounts:
    if amount > 1000:
        print(f"Manual review needed for {amount}")
        break

Use continue to skip the rest of the current iteration.

amounts = [120, 0, 340, 0, 500]

for amount in amounts:
    if amount == 0:
        continue
    print(f"Processing amount: {amount}")

These two statements are useful, but do not overuse them. Too many early exits can make a loop harder to follow.

while Loops

A while loop keeps running while a condition is true.

remaining_retries = 3

while remaining_retries > 0:
    print(f"Retries left: {remaining_retries}")
    remaining_retries -= 1

A common mistake is forgetting to update the condition. That can create an infinite loop.

List Comprehensions

A list comprehension creates a new list from an existing sequence. It can often replace a small loop.

Here is the loop version:

amounts = [100, 250, 80, 410]
amounts_with_tax = []

for amount in amounts:
    amounts_with_tax.append(amount * 1.25)

print(amounts_with_tax)

Here is the list comprehension version:

amounts = [100, 250, 80, 410]
amounts_with_tax = [amount * 1.25 for amount in amounts]

print(amounts_with_tax)

You can also filter values.

orders = [
    {"id": "A100", "amount": 320.0, "paid": True},
    {"id": "A101", "amount": 180.0, "paid": False},
    {"id": "A102", "amount": 510.0, "paid": True},
]

paid_order_ids = [order["id"] for order in orders if order["paid"]]
print(paid_order_ids)

List comprehensions are best when the transformation is short. If the logic needs several steps, use a normal loop. Readability matters more than cleverness.

Functions: Reusable Units of Work

A function groups code into a named operation. It can accept input arguments and return a result.

def calculate_total(amount, tax_rate=0.25):
    return amount * (1 + tax_rate)

print(calculate_total(100))
print(calculate_total(100, tax_rate=0.12))

The first argument, amount, is required. The second argument, tax_rate, has a default value, so it is optional.

Positional and Keyword Arguments

You can pass arguments by position.

def build_label(name, country):
    return f"{name} ({country})"

print(build_label("Sara", "Sweden"))

You can also pass them by name.

print(build_label(country="Norway", name="Jon"))

Keyword arguments often make code clearer, especially when a function accepts multiple values of the same type.

Returning None

A function returns None if it does not return anything explicitly.

def print_status(order_id, paid):
    if paid:
        print(f"Order {order_id} is paid")
    else:
        print(f"Order {order_id} is unpaid")

result = print_status("A100", True)
print(result)

That final print(result) displays None. The function performs an action, but it does not produce a returned value.

A Practical Function Example

This function normalizes country names from messy input.

def normalize_country(value):
    if value is None:
        return "Unknown"

    cleaned = value.strip().lower()

    if cleaned == "se":
        return "Sweden"
    elif cleaned == "no":
        return "Norway"
    elif cleaned == "de":
        return "Germany"
    else:
        return cleaned.title()

print(normalize_country(" se "))
print(normalize_country("germany"))
print(normalize_country(None))

This shows several useful ideas in one place:

  • Accept messy input.
  • Handle missing values.
  • Normalize text before comparing.
  • Return clean output.

Modules: Splitting Code Across Files

As scripts grow, keeping everything in one file becomes painful. A module is a Python file that can be imported from another Python file or notebook.

Imagine a file named cleaning_tools.py:

VALID_COUNTRY_CODES = ("se", "no", "de")


def normalize_country(value):
    if value is None:
        return "Unknown"

    cleaned = value.strip().lower()

    if cleaned == "se":
        return "Sweden"
    elif cleaned == "no":
        return "Norway"
    elif cleaned == "de":
        return "Germany"
    else:
        return cleaned.title()

You can import and use it from another file in the same folder.

import cleaning_tools

country = cleaning_tools.normalize_country(" se ")
print(country)
print(cleaning_tools.VALID_COUNTRY_CODES)

You can also use an alias.

import cleaning_tools as ct

print(ct.normalize_country("no"))

Another option is to import specific objects.

from cleaning_tools import normalize_country

print(normalize_country("de"))

Using import cleaning_tools or import cleaning_tools as ct makes it clear where the function comes from. Importing specific objects can be convenient, but it can also make larger files harder to understand if too many names come from different modules.

Working with Dates and Times

Python's standard library includes the datetime module for date and time work. A common convention is to import it with the alias dt.

import datetime as dt

created_at = dt.datetime(2026, 4, 15, 9, 30)
print(created_at)
print(created_at.year)
print(created_at.month)
print(created_at.day)

A datetime object stores a specific date and time. You can compare dates, subtract them, or add time intervals.

import datetime as dt

created_at = dt.datetime(2026, 4, 15, 9, 30)
reviewed_at = dt.datetime(2026, 4, 18, 14, 45)

age = reviewed_at - created_at
print(age)

follow_up_at = created_at + dt.timedelta(days=7)
print(follow_up_at)

Formatting a date into text is common when building file names, messages, or reports.

import datetime as dt

created_at = dt.datetime(2026, 4, 15, 9, 30)

label = f"{created_at:%Y-%m-%d %H:%M}"
print(label)

You can also parse text into a datetime object.

import datetime as dt

raw_date = "15.04.2026"
parsed_date = dt.datetime.strptime(raw_date, "%d.%m.%Y")

print(parsed_date)

Date handling is an area where clarity matters. Always make it obvious what format your input text uses.

Code Style: Make Python Easy to Read

Python code is read more often than it is written. Good style makes your code easier to debug and maintain.

Here are practical rules that matter early:

  • Use lowercase variable and function names with underscores, such as total_amount.
  • Use uppercase names for constants, such as MAX_RETRIES.
  • Put imports at the top of the file.
  • Use four spaces for indentation.
  • Keep functions small and focused.
  • Use comments to explain why something exists, not what the code already says.
  • Prefer clear names over short names.

A simple, readable Python file could look like this:

import datetime as dt

SUPPORTED_STATUSES = ("new", "paid", "cancelled")


def is_supported_status(status):
    return status.lower() in SUPPORTED_STATUSES


def build_audit_message(order_id, status):
    created_at = dt.datetime.now()
    timestamp = f"{created_at:%Y-%m-%d %H:%M}"
    return f"{timestamp}: order {order_id} changed to {status}"


if is_supported_status("paid"):
    print(build_audit_message("A100", "paid"))

This example uses constants, functions, imports, date formatting, and readable names. It is small, but it already has a structure that can grow.

Formatting and Linting with Ruff

Manual formatting is boring and easy to get wrong. A formatter rewrites code into a consistent style. A linter checks code for style issues and likely mistakes without running the program.

Ruff can do both formatting and linting.

If you are using a project managed with uv, you can run Ruff like this:

uv run ruff format cleaning_tools.py
uv run ruff check cleaning_tools.py

The formatter handles layout. The linter can catch issues such as unused imports.

For example, this file imports a module it does not use:

import os


def calculate_discount(amount):
    if amount >= 1000:
        return amount * 0.10
    return 0

A linter can flag the unused os import. That kind of feedback is valuable because unused imports make code noisy and can hide real mistakes.

In an editor such as VS Code, Ruff can also be used through an extension, so issues appear directly while editing.

Type Hints: Optional Clarity for Humans and Tools

Python does not require type declarations, but you can add type hints. A type hint tells readers and developer tools what kind of value a function expects or returns.

def calculate_net_amount(gross_amount: float, tax_rate: float = 0.25) -> float:
    return gross_amount / (1 + tax_rate)

print(calculate_net_amount(125.0))

The function still runs as normal Python code. The type hints do not change the runtime behavior by themselves. Their main value is communication:

  • They help editors provide better suggestions.
  • They document the expected input and output.
  • They make type-related mistakes easier to notice.

Use type hints where they improve understanding. For very small scripts, you may not need them everywhere. For reusable functions, they are often worth adding.

A Complete Mini Example

The following example combines many of the foundations in a small data-cleaning workflow. It takes a list of order records, normalizes country values, filters paid orders, calculates totals, and creates simple audit messages.

import datetime as dt

TAX_RATE = 0.25


def normalize_country(value):
    if value is None:
        return "Unknown"

    cleaned = value.strip().lower()

    if cleaned == "se":
        return "Sweden"
    elif cleaned == "no":
        return "Norway"
    elif cleaned == "de":
        return "Germany"
    else:
        return cleaned.title()


def calculate_total(amount, tax_rate=TAX_RATE):
    return amount * (1 + tax_rate)


def build_audit_line(order_id, country, total):
    created_at = dt.datetime.now()
    timestamp = f"{created_at:%Y-%m-%d %H:%M}"
    return f"{timestamp}: {order_id} from {country} has total {total:.2f}"


orders = [
    {"id": "A100", "country": " se ", "amount": 320.0, "paid": True},
    {"id": "A101", "country": "Norway", "amount": 180.0, "paid": False},
    {"id": "A102", "country": None, "amount": 510.0, "paid": True},
]

for order in orders:
    if not order["paid"]:
        continue

    country = normalize_country(order["country"])
    total = calculate_total(order["amount"])
    audit_line = build_audit_line(order["id"], country, total)

    print(audit_line)

This is still beginner-level Python, but it represents a real pattern:

  1. Store input data in a useful structure.
  2. Write small functions for repeated behavior.
  3. Normalize messy values before using them.
  4. Use conditions to skip irrelevant records.
  5. Format output clearly.

Once you understand this pattern, moving to larger data tools becomes much easier.

Common Mistakes to Watch For

Confusing Assignment and Equality

Use = for assignment. Use == for equality checks.

status = "paid"

if status == "paid":
    print("Payment received")

Forgetting Zero-Based Indexing

The first item is index 0.

values = [10, 20, 30]
print(values[0])

Using the Wrong Data Structure

Do not use a list when your values have names.

customer = [1001, "Nina", "Sweden"]

This works, but it is not clear. A dictionary is easier to understand.

customer = {
    "id": 1001,
    "name": "Nina",
    "country": "Sweden",
}

Writing Too Much Code Outside Functions

A quick script can start with top-level code, but repeated logic should become a function.

def is_large_order(amount):
    return amount >= 1000

Small functions are easier to test, reuse, and debug.

Making List Comprehensions Too Clever

This is readable:

paid_ids = [order["id"] for order in orders if order["paid"]]

If the expression becomes long or nested, switch to a normal loop.

Ignoring Code Formatting

Inconsistent formatting makes code harder to scan. Use a formatter instead of debating spaces manually.

uv run ruff format your_file.py

Beginner Checklist

Use this checklist when writing a new Python script:

  • Have I chosen clear variable names?
  • Do I know the data type of each important value?
  • Am I using a list, dictionary, tuple, or set for the right reason?
  • Are conditions readable and not overly nested?
  • Are repeated operations moved into functions?
  • Are missing values handled explicitly with None where appropriate?
  • Are strings normalized before comparison when user input is involved?
  • Are dates parsed and formatted with an obvious format?
  • Are imports placed at the top of the file?
  • Have I formatted and linted the file?

Conclusion

Python foundations are not just syntax. They are the habits that make scripts reliable: clear names, correct data structures, readable conditions, small functions, reusable modules, and consistent formatting.

Start with simple values and collections. Practice indexing, slicing, loops, and functions until they feel natural. Then add modules, date handling, formatting tools, and type hints as your scripts grow. That path gives you a practical base for automation, data cleaning, and analysis work without making Python feel larger than it needs to be.

Share:

Comments0

Home Profile Menu Sidebar
Top