Python & Data Science

Chapter 3
Professional Python Programming

Advance Diploma in Artificial Intelligence and Machine Learning
Python & Data Science   •   Advance Diploma

Professional Python Programming

Introduction

Professional Python programming is more than writing code that produces the correct output. In data science and artificial intelligence, programs must also be understandable, testable, reproducible, secure, efficient enough for their purpose, and maintainable by people other than their original author. A short script may be adequate for a one-time exploration, but a production data pipeline, model-serving service, or research library requires disciplined engineering practices.

This chapter presents the practices that transform Python code into reliable software. It covers object-oriented and functional principles, exception handling, file management, iterators and generators, decorators, type hints, logging, testing, debugging, documentation, virtual environments, Git, coding standards, and introductory performance optimization. These techniques are particularly important in AI and data science, where programs often combine heterogeneous data sources, numerical libraries, experimental code, and operational systems.

Learning Objectives

By the end of this chapter, the learner should be able to:

  • design maintainable Python programs using suitable procedural, object-oriented, and functional techniques;
  • handle errors deliberately and distinguish recoverable failures from programming defects;
  • process files and large data streams safely using context managers, iterators, and generators;
  • use decorators, type hints, documentation, and logging to improve code quality;
  • create isolated environments and use Git-based workflows;
  • apply coding standards, automated tests, debugging methods, and basic profiling;
  • evaluate professional, ethical, and operational risks in Python-based AI systems.

1. Maintainability as a Software Quality Goal

Maintainability is the ease with which software can be corrected, adapted, understood, tested, and extended. It is influenced by several related qualities:

QualityMeaning in practice
CorrectnessThe program meets its stated requirements.
ReadabilityA competent developer can understand the code.
ModularityResponsibilities are divided into cohesive components.
TestabilityBehaviour can be verified automatically.
ReproducibilityThe same environment and inputs produce comparable results.
ObservabilityLogs and diagnostics reveal what the program is doing.
SecurityInputs, secrets, permissions, and dependencies are managed safely.

A useful design principle is separation of concerns: data acquisition, validation, transformation, modelling, persistence, and presentation should not be unnecessarily entangled. A function that downloads data, cleans it, trains a model, and prints a report is difficult to reuse and test. Several smaller components with explicit interfaces are generally preferable.

The principle of single responsibility does not mean that every function must contain one line. It means that a component should have one coherent reason to change. The principles of DRY (Do Not Repeat Yourself), KISS (Keep It Simple), and YAGNI (You Aren't Gonna Need It) discourage duplication, unnecessary complexity, and speculative features. They must be applied with judgement: removing all duplication can create excessive abstraction, while excessive simplicity can ignore legitimate domain complexity.

2. Structuring Python Programs

A professional Python project commonly separates source code, tests, documentation, configuration, and generated artefacts. A possible structure is:

project/
├── pyproject.toml
├── README.md
├── src/
│   └── demand_forecast/
│       ├── __init__.py
│       ├── io.py
│       ├── features.py
│       └── model.py
├── tests/
│   ├── test_features.py
│   └── test_model.py
└── notebooks/

The src layout helps prevent accidental imports from the project root. Notebooks are useful for exploration, but reusable logic should normally be moved into modules. Configuration such as file paths, model parameters, and service endpoints should not be scattered throughout code. It should be supplied through configuration files, environment variables, or command-line arguments, with secrets kept outside version control.

A module is a Python file containing definitions. A package is a collection of related modules. Public interfaces should be deliberately chosen; internal implementation details can be prefixed with an underscore by convention. A package's __init__.py may expose selected functions, but indiscriminate re-exporting can obscure where behaviour is implemented.

3. Object-Oriented Programming

Object-oriented programming (OOP) groups data and operations that belong together. A class defines a type, while an object is an instance of that type. Encapsulation protects invariants—conditions that should remain true for a valid object. Inheritance allows a type to reuse or specialise another type, but composition—building objects from other objects—is often less tightly coupled and easier to change.

from dataclasses import dataclass

@dataclass(frozen=True)
class Measurement:
    sensor_id: str
    value: float
    unit: str

    def is_valid(self, minimum: float, maximum: float) -> bool:
        return minimum <= self.value <= maximum

The @dataclass decorator generates useful methods such as __init__ and __repr__. frozen=True makes instances effectively immutable after construction, which can reduce accidental state changes. Immutability is especially helpful when objects are shared across processing stages.

Important OOP concepts include:

  • Abstraction: exposing essential behaviour without requiring users to know implementation details;
  • Encapsulation: controlling access to state and preserving invariants;
  • Polymorphism: allowing different objects to respond to a common operation;
  • Inheritance: deriving a specialised class from a general class;
  • Composition: delegating work to contained objects.

Abstract interfaces can express required operations:

from abc import ABC, abstractmethod

class Predictor(ABC):
    @abstractmethod
    def predict(self, features: list[float]) -> float:
        """Return a prediction for one feature vector."""
        raise NotImplementedError

A common error is to create classes merely because classes are available. A small stateless transformation may be clearer as a function. OOP is most valuable when an object has meaningful state, invariants, lifecycle, or interchangeable implementations.

4. Functional Programming Techniques

Functional programming treats computation as the evaluation and combination of functions. Python is multi-paradigm, so functional ideas complement rather than replace OOP.

A pure function returns a value determined only by its inputs and has no observable side effects. Pure functions are easy to test and reason about:

def normalise(value: float, minimum: float, maximum: float) -> float:
    if maximum <= minimum:
        raise ValueError("maximum must exceed minimum")
    return (value - minimum) / (maximum - minimum)

The formula used is:

z = (x − xmin) / (xmax − xmin)

Here, x is the value being transformed and x_min, x_max are the minimum and maximum of the reference range. The formula is applicable when x_max > x_min; otherwise the denominator is zero or negative and the intended scaling is undefined. Values outside the reference range produce values below 0 or above 1 unless they are clipped separately.

List comprehensions, map, filter, sorted, any, and all can express transformations clearly. However, a comprehension is often more readable than nested uses of map and filter. Avoid excessively clever one-liners. Functional design also encourages immutable inputs, explicit return values, and minimal hidden state.

5. Exceptions and Defensive Error Handling

An exception indicates that normal execution cannot continue in its current form. A professional program does not simply catch every exception:

try:
    value = int(text)
except ValueError:
    value = 0

The try block should be narrow enough that the handler does not accidentally hide unrelated defects. Catch the most specific exception that can be handled. Catching Exception may be appropriate at an application boundary—for example, to log an unhandled failure before termination—but it is usually inappropriate inside ordinary business logic.

Use raise to report invalid states and exception chaining to preserve the original cause:

try:
    data = load_record(path)
except OSError as exc:
    raise RuntimeError(f"Could not load record: {path}") from exc

Custom exceptions communicate domain meaning:

class InvalidRecordError(ValueError):
    pass

Do not use exceptions as ordinary control flow when a simple condition expresses the situation more clearly. Error messages should state what failed and, where safe, identify relevant context. They must not expose passwords, tokens, personal data, or sensitive model inputs.

6. Files, Paths, and Context Managers

Files should be opened with a context manager so that they are closed even when an exception occurs:

from pathlib import Path

path = Path("data") / "records.txt"
with path.open("r", encoding="utf-8") as file:
    for line in file:
        process(line.rstrip("\n"))

pathlib.Path is generally preferable to manual string concatenation because it expresses filesystem operations portably. Always specify text encoding when portability matters. Binary files should be opened with modes such as "rb" or "wb".

A context manager defines setup and cleanup behaviour through __enter__ and __exit__. It can protect database connections, temporary resources, locks, and files. For large datasets, process records incrementally rather than loading the entire file into memory. Validate input structure, reject unexpected formats, and write output to a temporary file before replacing a target when partial output would be harmful.

Serialization deserves care. JSON is readable and widely interoperable, but it supports fewer data types than Python. Python pickle can execute arbitrary code during deserialization and must not be used with untrusted data. Data exchanged between systems should use a documented, validated format.

7. Iterators and Generators

An iterable can produce values one at a time. An iterator maintains iteration state and implements __next__; it raises StopIteration when exhausted. A generator function, identified by yield, creates an iterator lazily:

def valid_lines(path: Path):
    with path.open(encoding="utf-8") as file:
        for line in file:
            text = line.strip()
            if text and not text.startswith("#"):
                yield text

A generator supports streaming and can reduce peak memory use. If n records are processed one at a time, the data portion of memory can be approximately constant, O(1), rather than O(n), although buffers and downstream structures may change this result. Laziness also means errors may occur only when the generator is consumed, not when it is created.

Iterators are especially useful in ETL pipelines:

records = read_records(source)
cleaned = (clean(record) for record in records)
selected = (record for record in cleaned if record.score >= 0.8)
for record in selected:
    save(record)

A generator is single-use. If values must be traversed repeatedly, materialise them deliberately or create a re-iterable collection.

8. Decorators

A decorator is a callable that receives a function or class and returns a modified callable. It supports cross-cutting concerns such as logging, timing, caching, access control, and retries.

from functools import wraps
import time


def timed(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return function(*args, **kwargs)
        finally:
            elapsed = time.perf_counter() - start
            print(f"{function.__name__}: {elapsed:.4f}s")
    return wrapper

@wraps preserves metadata such as the function name and documentation. Decorators should preserve return values and exception behaviour unless changing them is an explicit design decision. A retry decorator must be particularly careful: retrying a read may be safe, whereas retrying a payment or database write may duplicate an operation unless it is idempotent.

9. Type Hints and Documentation

Type hints communicate intended interfaces and allow tools to detect inconsistencies. They do not automatically enforce types at runtime.

from collections.abc import Sequence

def mean(values: Sequence[float]) -> float:
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

Hints should describe meaningful contracts, not merely decorate every local variable. Use Optional-style unions such as str | None in supported Python versions, and use TypedDict, data classes, or protocol definitions when structured interfaces need documentation.

A docstring should explain purpose, parameters, return value, raised exceptions, side effects, and important assumptions. For example:

def read_threshold(config: dict[str, float], name: str) -> float:
    """Return a non-negative threshold from configuration.

    Raises:
        KeyError: If name is absent.
        ValueError: If the configured threshold is negative.
    """
    value = config[name]
    if value < 0:
        raise ValueError("threshold must be non-negative")
    return value

Documentation should be maintained with the code. A README should normally include installation, usage, configuration, testing, and contribution instructions. API documentation tools can generate reference material from docstrings, but generated documentation cannot replace conceptual explanations.

10. Logging, Debugging, and Observability

print is useful for a quick experiment; logging is appropriate for software that must be operated and diagnosed. A log record commonly includes a timestamp, severity, logger name, message, and contextual identifiers.

import logging

logger = logging.getLogger(__name__)

logger.info("Starting batch", extra={"batch_id": batch_id})
logger.warning("Missing optional field: %s", field_name)

The standard severity levels are DEBUG, INFO, WARNING, ERROR, and CRITICAL. Configure handlers and formats at the application boundary rather than inside reusable libraries. Use lazy formatting (logger.info("Count: %s", count)) rather than constructing messages unnecessarily. Never log credentials or unmasked personal information.

Debugging is a systematic process:

  1. reproduce the problem with the smallest reliable input;
  2. state the expected and actual behaviour;
  3. inspect the traceback and identify the failing boundary;
  4. test hypotheses using a debugger, assertions, or targeted logs;
  5. make the smallest justified change;
  6. add a regression test;
  7. verify related behaviour.

An assertion expresses an internal invariant, not user input validation. Assertions may be disabled when Python is run with optimisation, so security and business checks must use ordinary conditional logic.

11. Testing Strategies

Testing provides evidence about behaviour; it does not prove that all defects are absent. A test should be deterministic, isolated, focused, and readable.

  • Unit tests verify small components.
  • Integration tests verify collaboration between components such as a parser and a database.
  • System or end-to-end tests exercise a complete workflow.
  • Regression tests prevent a previously corrected defect from returning.
  • Property-based tests check general properties over many generated inputs.

A simple unit test using pytest might be:

def test_normalise_midpoint():
    assert normalise(5, 0, 10) == 0.5

def test_normalise_rejects_equal_bounds():
    import pytest
    with pytest.raises(ValueError):
        normalise(5, 3, 3)

Test boundaries, empty inputs, malformed records, missing files, extreme values, and permission failures. Mock external services when testing local logic, but include integration tests that verify the real interface. Excessive mocking can produce tests that pass while the actual components are incompatible.

For machine-learning code, test data schemas, feature transformations, leakage controls, deterministic preprocessing, model input shapes, and evaluation procedures. A model score alone is not a sufficient software test. Tests should also check reproducibility and the correct separation of training and evaluation data.

12. Virtual Environments and Dependency Management

A virtual environment isolates a project's interpreter and installed packages from other projects. This prevents incompatible dependency versions from interfering with one another.

python -m venv .venv
# Linux or macOS
source .venv/bin/activate
# Windows PowerShell
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip

Dependencies should be declared in project metadata, commonly pyproject.toml, and versions should be managed deliberately. A lock file or pinned environment specification improves reproducibility. Reproducibility also depends on the operating system, hardware, random seeds, data versions, and external services; installing identical packages alone is not a complete guarantee.

Do not commit .venv, generated datasets, credentials, or large model artefacts unless policy explicitly requires them. Use secret-management systems or environment variables for sensitive configuration.

13. Git and Collaborative Development

Git records changes as a directed history of commits. A professional commit should represent one coherent change and have a meaningful message. A basic workflow is:

git status
git switch -c feature/input-validation
git add src/ tests/
git commit -m "Validate incoming records"
git push -u origin feature/input-validation

Branches support parallel work, while pull requests enable review, automated testing, and discussion. .gitignore prevents local environments, caches, secrets, and generated files from entering the repository. Never assume that deleting a secret from the latest commit removes it from history; exposed credentials should be revoked and repository history handled according to incident procedures.

Code review should examine correctness, clarity, security, tests, error handling, performance assumptions, and compatibility. Automated checks such as formatters, linters, type checkers, and test suites should run before merging.

14. Coding Standards and Performance

PEP 8 provides widely adopted Python style guidance. Consistent naming, four-space indentation, short focused functions, sensible line lengths, and explicit imports reduce cognitive overhead. Tools such as formatters and linters make style checks repeatable rather than dependent on personal preference.

Performance work should begin with a requirement and measurement. The practical sequence is:

  1. define an acceptable response time or throughput;
  2. measure representative workloads;
  3. identify the bottleneck with profiling;
  4. change the highest-impact component;
  5. measure again and confirm correctness.

Big-O notation describes how resource use grows with input size. For example, scanning a list once is O(n), while comparing every pair is generally O(n²). This describes growth, not exact elapsed time; constants, memory locality, I/O, interpreter overhead, and libraries also matter.

Common improvements include choosing sets or dictionaries for frequent membership lookup, avoiding repeated work, streaming large files, using vectorised numerical operations where appropriate, and selecting suitable algorithms. Caching can improve speed but introduces invalidation, memory, and consistency problems. Parallelism is not automatically beneficial: Python threads are often useful for I/O-bound work, while CPU-bound numerical work may require vectorised native libraries, multiprocessing, or specialised systems. Profile before optimising.

15. Applied Case: A Reliable Demand-Data Pipeline

Background

The following is an illustrative case. A retailer receives daily demand records from several branches. An initial script reads a CSV file, removes invalid rows, calculates features, and trains a forecasting model. It fails when a file is missing, loads very large files into memory, prints diagnostic messages, and cannot be tested reliably.

Problem Analysis

The script has several risks: file handling is not guaranteed to clean up resources; malformed rows are silently discarded; business validation is mixed with model training; configuration is hard-coded; logs may expose customer information; and changes cannot be reviewed systematically.

Approach

The team divides the system into components:

  1. an input adapter yields records lazily from a file;
  2. a validator returns a validated data object or a domain exception;
  3. a feature module contains pure functions;
  4. a model component implements a predictor interface;
  5. an application service coordinates the workflow;
  6. tests cover valid data, malformed data, empty input, and model boundaries.

A simplified streaming boundary is:

from collections.abc import Iterator
from pathlib import Path
import csv


def rows(path: Path) -> Iterator[dict[str, str]]:
    with path.open(newline="", encoding="utf-8") as file:
        yield from csv.DictReader(file)

The application catches FileNotFoundError at its boundary, logs a safe operational message, and returns a failure status suitable for the scheduler. It does not catch ValueError inside every function; validation errors are allowed to carry their meaning to the layer responsible for deciding whether to reject the batch or quarantine the record.

The repository includes a virtual-environment specification, a README, a test suite, a formatter and linter configuration, and a Git workflow. Data quality metrics—such as accepted, rejected, and missing records—are logged as aggregate values rather than raw personal information.

Outcome

The revised pipeline uses bounded memory for input processing, exposes reusable transformations, provides actionable diagnostics, and makes defects visible through automated tests. The model itself may still be inaccurate; software quality does not guarantee predictive quality. However, the improved architecture makes data leakage, schema drift, and operational failures easier to detect and manage.

Learning Points

The case demonstrates that professional programming is an end-to-end discipline. Generators address scale, exceptions clarify failure, type hints document contracts, tests protect behaviour, logging supports operations, and Git plus environments support collaboration and reproducibility.

16. Limitations, Ethics, and Governance

Engineering practices can reduce risk but cannot eliminate it. Type hints do not validate untrusted data, tests may omit important cases, logs can become a privacy liability, and reproducible code can still reproduce biased decisions. AI systems require attention to data provenance, consent, fairness, explainability, security, and human oversight.

Input validation should protect against malformed data and injection attacks. Dependencies should be monitored for known vulnerabilities and upgraded through a controlled process. Access should follow least privilege. Personal data should be minimised, retained only as long as justified, and protected according to applicable law and institutional policy.

In regulated or high-impact applications, teams should preserve versioned code, data descriptions, model artefacts, evaluation results, approvals, and incident records. A reproducible experiment is not automatically an ethically acceptable deployment. Technical documentation should state assumptions, known limitations, performance across relevant groups, and conditions under which the system should not be used.

Contemporary Developments

Modern Python teams increasingly use continuous integration, static type checking, dependency scanning, containerised deployment, data and model versioning, and automated documentation. Data pipelines are becoming observable systems with lineage, quality checks, and monitoring for distribution shift. The growing use of AI coding assistants makes human review more important: generated code can contain insecure dependencies, licensing problems, incorrect assumptions, or subtle data leakage. The same standards—tests, review, provenance, and security analysis—must apply regardless of who or what produced the code.

Best-Practice Checklist

  • Give each component a clear responsibility and interface.
  • Prefer pure functions for transformations where practical.
  • Use context managers for resources.
  • Stream large inputs with iterators or generators.
  • Catch specific exceptions and preserve causes.
  • Add type hints and useful docstrings to public interfaces.
  • Use structured, appropriately configured logging.
  • Test normal, boundary, invalid, and failure cases.
  • Isolate dependencies in a virtual environment.
  • Commit small, coherent changes and review them.
  • Measure before optimising and verify performance after changes.
  • Protect secrets and personal data throughout the lifecycle.

Chapter Summary

Professional Python programming combines language features with disciplined software engineering. Maintainable systems separate concerns, expose clear interfaces, and use the simplest suitable programming paradigm. Object-oriented design is useful for state, invariants, and interchangeable components; functional techniques support pure, composable transformations. Exceptions should be specific and intentional, while context managers ensure resource cleanup. Iterators and generators enable lazy, memory-conscious processing, and decorators provide reusable cross-cutting behaviour.

Type hints, docstrings, logging, debugging, automated tests, virtual environments, Git, and coding standards make programs easier to understand, reproduce, operate, and improve. Performance optimisation should be evidence-based and guided by profiling. In AI and data science, these practices must be combined with data validation, security, privacy, fairness, provenance, and human oversight. High-quality code is not merely code that runs; it is code whose behaviour, limitations, and operational consequences can be understood and responsibly managed.

Key Terms

  • Abstraction: Representation of essential behaviour while hiding unnecessary implementation detail.
  • Context manager: Object that defines setup and cleanup around a controlled block.
  • Decorator: Callable that wraps or modifies another callable.
  • Encapsulation: Protection and controlled management of an object's internal state.
  • Exception: Object representing an abnormal condition during execution.
  • Generator: Function that yields values lazily and returns an iterator.
  • Git: Distributed version-control system for tracking collaborative changes.
  • Immutability: Property of an object whose state cannot be changed after creation.
  • Iterator: Object that produces a sequence of values through __next__.
  • Logging: Recording operational events for diagnosis and monitoring.
  • Maintainability: Ease of understanding, correcting, adapting, and extending software.
  • Mock: Test substitute that simulates a dependency.
  • Pure function: Function whose result depends only on inputs and has no observable side effects.
  • Refactoring: Improving internal structure without intentionally changing external behaviour.
  • Regression test: Test that prevents a previously fixed defect from returning.
  • Separation of concerns: Division of a system into components with distinct responsibilities.
  • Type hint: Annotation communicating intended types to developers and tools.
  • Virtual environment: Isolated Python environment for project dependencies.

Review Questions

5 Short Answer Questions

  1. What is the purpose of separation of concerns in a Python project?
  2. Distinguish an iterable from an iterator.
  3. Why should exception handlers usually catch specific exception classes?
  4. What information can logging provide that a simple print statement may not?
  5. Why are virtual environments useful in data-science projects?

5 Long Answer Questions

  1. Explain how object-oriented and functional programming techniques can be combined in a maintainable data pipeline.
  2. Discuss the design and operational advantages of generators when processing large files.
  3. Describe a professional testing strategy for a machine-learning preprocessing component.
  4. Explain how Git, code review, automated checks, and dependency management contribute to reproducibility.
  5. Discuss why performance optimisation should be guided by profiling rather than intuition.

3 Analytical / Application Questions

  1. A program catches Exception around an entire data-processing workflow and logs only “failed.” Analyse the risks and propose a more reliable error-handling design.
  2. Design a Python architecture for a service that reads records, validates them, computes features, and invokes two interchangeable prediction models. Identify suitable modules, interfaces, tests, and logging events.
  3. A pipeline processes ten million records and currently loads them into a list before filtering. Explain how you would redesign it using iterators or generators, and identify conditions under which the redesign would not substantially reduce total memory use.

Multiple Choice Questions with Answers

Question 1

Which design principle most directly recommends separating data loading from model training?

A. Separation of concerns

B. Inheritance

C. Operator overloading

D. Duck typing

Correct Answer: A. Separation of concerns

Question 2

What does a generator function normally return when it is called?

A. A fully materialised list

B. An iterator that produces values lazily

C. A database connection

D. A compiled machine-learning model

Correct Answer: B. An iterator that produces values lazily

Question 3

Which practice ensures that a file is closed when an exception occurs inside the block?

A. Using a global variable

B. Using a context manager

C. Using a list comprehension

D. Using inheritance

Correct Answer: B. Using a context manager

Question 4

Why is functools.wraps useful in a decorator?

A. It converts a function into a generator

B. It preserves wrapped-function metadata

C. It catches every exception automatically

D. It makes all arguments integers

Correct Answer: B. It preserves wrapped-function metadata

Question 5

What is the main limitation of Python type hints?

A. They prevent functions from returning values

B. They automatically encrypt variables

C. They do not by themselves enforce types at runtime

D. They can be used only in classes

Correct Answer: C. They do not by themselves enforce types at runtime

Question 6

Which log level generally indicates a serious failure that may require immediate attention?

A. DEBUG

B. INFO

C. WARNING

D. CRITICAL

Correct Answer: D. CRITICAL

Question 7

What is the primary purpose of a regression test?

A. To increase file size

B. To prevent a corrected defect from returning

C. To replace all integration tests

D. To install dependencies

Correct Answer: B. To prevent a corrected defect from returning

Question 8

Which action is most appropriate before attempting a performance optimisation?

A. Remove all error handling

B. Measure and profile representative workloads

C. Convert every function into a class

D. Duplicate the data in memory

Correct Answer: B. Measure and profile representative workloads

Question 9

Why should pickle data from an untrusted source generally be avoided?

A. It cannot represent Python objects

B. Deserialisation can execute arbitrary code

C. It always produces invalid JSON

D. It works only with text files

Correct Answer: B. Deserialisation can execute arbitrary code

Question 10

Which item should normally be excluded from a Git repository?

A. A unit test

B. A documented source module

C. A file containing production credentials

D. A README file

Correct Answer: C. A file containing production credentials

Python & Data Science
Chapter 3 of 15