Overview Most developers treat AI as a magic wand that spits out finished applications in minutes. This mindset creates a significant long-term problem: unmanageable complexity. When you ask an AI to "build a dashboard," it often generates a monolithic block of code that works initially but breaks the moment you need to scale or modify it. Proper software design is your primary tool for managing this complexity. By applying design principles, you reduce the cognitive load on the AI, making your prompts clearer and the resulting code more maintainable. This tutorial explores how to guide an AI coding assistant like ChatGPT through the iterative process of building a robust animation system in Python. Prerequisites To follow this guide, you should be comfortable with basic Python syntax and object-oriented programming. Familiarity with Abstract Base Classes (ABCs) or Protocols is helpful. You should also understand the concept of a "loop" in the context of graphics, specifically how a canvas updates over time. Key Libraries & Tools * **Python 3.10+**: Uses modern type annotations and lowercase collection types. * **Tkinter (TK)**: The standard GUI library used here for canvas rendering. * **ChatGPT**: The LLM used to generate and refactor code iterations. * **typing**: A built-in module for providing type hints and protocols. Code Walkthrough 1. Defining the Animation Protocol We start by decoupling the animation logic from the runner. Instead of a massive `if` statement checking for "move" or "rotate," we define a protocol. This ensures every animation step follows a predictable structure. ```python from typing import Protocol class AnimationStep(Protocol): def apply(self, shape_id: int, renderer: "GraphicsRenderer", t: float) -> None: ... ``` 2. Implementing Decoupled Commands By turning each action into its own class (the Command Pattern), we make the system extensible. Notice how `MoveStep` doesn't know about the internal state of the `Animator`; it simply receives what it needs to perform its specific transformation. ```python from dataclasses import dataclass @dataclass class MoveStep: start_pos: tuple[float, float] end_pos: tuple[float, float] def apply(self, shape_id: int, renderer: "GraphicsRenderer", t: float) -> None: # Interpolate between start and end based on time t curr_x = self.start_pos[0] + (self.end_pos[0] - self.start_pos[1]) * t # ... apply to renderer ``` 3. Separation of Concerns: The Renderer A common AI mistake is giving one class too many jobs. Initially, the `Animator` handled the canvas, the shapes, and the timing. We refactor this by creating a `GraphicsRenderer` that only cares about low-level operations: points and colors. ```python class GraphicsRenderer: def __init__(self, canvas): self.canvas = canvas self.items = {} def render(self, shape_id: int, points: list[float], color: str): if shape_id not in self.items: self.items[shape_id] = self.canvas.create_polygon(points, fill=color) else: self.canvas.coords(self.items[shape_id], *points) self.canvas.itemconfig(self.items[shape_id], fill=color) ``` 4. Refining the Playback Logic The final hurdle involves preventing cumulative errors. If you add to a shape's position every frame, the shape will eventually fly off the screen. We solve this by storing an "original state" at the start of the animation and calculating every frame relative to that baseline. Syntax Notes * **Lower-case Types**: Use `list[int]` and `dict[str, int]` instead of the deprecated uppercase `List` and `Dict` from the `typing` module. * **Protocols vs. ABCs**: While Abstract Base Classes work for inheritance, `Protocols` allow for structural subtyping (duck typing), which is often cleaner for animation steps. * **Dependency Injection**: Notice that the `GraphicsRenderer` accepts a `canvas` in its constructor. This makes the code easier to test and more flexible. Practical Examples This design approach is essential for any system where behavior changes over time. Beyond simple animations, you can apply these principles to: * **Data Pipelines**: Treating each processing step as a command. * **Game Development**: Separating entity logic from the rendering engine. * **UI Frameworks**: Decoupling event handling from the visual representation. Tips & Gotchas * **Circular Dependencies**: Watch out for classes that require each other (e.g., `Animator` needing `Step` while `Step` needs `Animator`). Solve this by using abstractions or moving methods to where the data lives. * **AI Context Drift**: LLMs often "forget" your previous design constraints, like lowercase type hints. You must be prepared to correct them multiple times. * **Cumulative Mutations**: Always prefer calculating state from a fixed starting point rather than adding small increments. Incremental updates lead to "drift" due to floating-point math errors.
Arjan
People
ArjanCodes (6 mentions) frames "Arjan" positively, with videos like "Coupling 101: Design Choices That Make or Break Your Code" and "I Built a Version of Honey That Actually SUPPORTS Creators" contributing to this sentiment.
- Jan 9, 2026
- Nov 28, 2025
- Aug 22, 2025
- Jun 6, 2025
- Jan 17, 2025
The New Interactive Interpreter and REPL Experience Python 3.13 introduces a transformed Read-Eval-Print Loop (REPL) that feels significantly more modern. The most immediate change involves color support; tracebacks and error messages now highlight specific syntax errors in vivid colors, making it faster to identify missing brackets or invalid arguments. Beyond aesthetics, the interpreter provides smarter suggestions. If you mistype a keyword argument like `maxsplit` as `MaxSplit`, the Python interpreter explicitly suggests the correct alternative. Furthermore, the REPL now supports direct commands. You no longer need to call `exit()`, `quit()`, or `help()` as functions with parentheses. Simply typing the word executes the command. For those pasting large code blocks, the new "paste mode" (toggled with F3) prevents the interpreter from misinterpreting indentation during the transfer, a common headache in previous versions. Refined Typing and Module Enhancements Static typing continues to evolve with the inclusion of PEP 705, which adds a `ReadOnly` qualifier for `TypedDict`. This allows developers to explicitly mark dictionary items as immutable for type checkers, though Python still ignores these at runtime. ```python from typing import TypedDict, ReadOnly class Point2D(TypedDict): x: float y: float label: ReadOnly[str] p = Point2D(x=1.1, y=2.2, label="Origin") Type checkers will flag the next line: p["label"] = "New Label" ``` The standard library also saw the removal of 19 "dead batteries"—deprecated modules like `crypt`, `telnetlib`, and `chunk`. Meanwhile, the `math` module gained a `fused multiply-add` (FMA) operation. `math.fma(x, y, z)` computes `(x * y) + z` with a single rounding step, preserving precision that is usually lost in intermediate calculations. Incremental Garbage Collection and Performance Foundations Python 3.13 revamps the cycle garbage collector to be incremental. Previously, a garbage collection cycle could pause the entire program (stop-the-world) to clear circular references. By performing this work in smaller increments, the interpreter reduces maximum pause times by an order of magnitude, especially for applications with large memory heaps. This version also introduces an experimental Just-In-Time (JIT) compiler. When enabled via the `--enable-experimental-jit` build flag, it converts some bytecode into machine code. While current benchmarks show negligible speedups—and even some regressions in I/O heavy tasks—this JIT serves as the structural foundation for massive performance leaps in future releases like Python 3.14. The Free-Threaded Interpreter and GIL Removal A separate executable now offers a "free-threaded" mode, allowing you to disable the Global Interpreter Lock. This is a monumental shift for multi-core processing. In standard Python, the GIL prevents multiple threads from executing Python bytecode simultaneously. With the GIL disabled, multi-threaded CPU-bound tasks can finally run in true parallel. However, this comes with a trade-off. Single-threaded code currently runs slightly slower in the free-threaded version due to the overhead of new thread-safety mechanisms. Developers must decide if the parallel throughput justifies the single-core performance hit. It remains an experimental feature, requiring specific installation (e.g., `3.13.0t` via pyenv), but it marks the beginning of a thread-safe, multi-core future for the language.
Oct 11, 2024Atomic Operations with Unit of Work Managing database consistency manually often leads to fragmented data and difficult debugging. The Unit of Work design pattern solves this by acting as a central collection point for multiple operations. Instead of sending every minor update to the database immediately, you gather them into a single logical transaction. This ensures that either every operation succeeds or the system rolls back to its original state, maintaining a reliable "all-or-nothing" integrity. Prerequisites & Tools To implement this pattern effectively, you should understand Python classes and basic database concepts like transactions and commits. While you can build this from scratch, most developers use SQLAlchemy, a powerful Python ORM. SQLAlchemy implements this pattern through its **Session** object, which tracks object changes and coordinates the final write-out. Code Walkthrough: Tracking State A simple implementation requires tracking which entities are new, modified ("dirty"), or slated for deletion. ```python class UnitOfWork: def __init__(self): self.new = [] self.dirty = [] self.removed = [] def register_new(self, entity): self.new.append(entity) def commit(self): # Logic to insert new, update dirty, and delete removed # If any step fails, trigger a rollback here pass ``` In this structure, calling `commit()` is the only way to persist data. This separation allows you to implement a rollback mechanism in the `except` block of your code, effectively undoing any partial changes that occurred before a failure. Syntax and Best Practices When using SQLAlchemy, utilize **Context Managers** (`with` statements) to handle your sessions. This ensures the session closes properly and provides a clean scope for your Unit of Work. If you need an ID from a newly created record before the final commit, use `session.flush()`. This pushes changes to the database buffer without ending the transaction, allowing the database to assign IDs while still permitting a full rollback if a later step fails. Beyond the Database This pattern isn't just for SQL. Think about **Infrastructure as Code**; if you provision a server but the database setup fails, you want to roll back the server creation so you aren't billed for unused resources. Similarly, file sync utilities like Dropbox use this logic to ensure a corrupt or partial file never replaces a healthy one during a network flicker.
May 10, 2024Overview of the Mojo Evolution Mojo represents a radical attempt to solve the "two-language problem" in high-performance computing. While Python dominates data science and AI through its simplicity, it often forces developers to rewrite critical paths in C or Rust to gain speed. Mojo aims to bridge this gap by offering a superset of Python syntax combined with the systems-level performance of a compiled language. It introduces static typing and an ownership model that empowers developers to write code that is both readable and remarkably fast. Prerequisites and Tooling To follow this guide, you should have a solid grasp of Python fundamentals, particularly functions and classes. Familiarity with memory management concepts like "ownership" from Rust is helpful but not mandatory. Currently, Mojo primarily supports **Unix** and **macOS** environments. Windows users must utilize the **Windows Subsystem for Linux (WSL)**. You will need the **Modular CLI** to install the Mojo compiler and run scripts. Core Syntax and Type Safety Mojo introduces the `fn` keyword for defining strictly typed functions, though it remains compatible with Python's `def` for dynamic behavior. Unlike Python, variables must be explicitly declared using `var` for mutable data or `let` for constants. ```python fn add_values(a: Int, b: Int) -> Int: let result = a + b return result fn main(): var x: Int = 5 x = 6 print(add_values(x, 10)) ``` In this walkthrough, `fn` ensures that the compiler checks types at build time. The `main` function serves as the explicit entry point, a departure from Python's script-style execution but standard for compiled languages. Structs, Traits, and Memory Ownership Instead of traditional classes, Mojo uses `structs`. These are fixed at compile time, providing better performance. You can implement `traits` (similar to Rust traits or Python protocols) to enforce specific behaviors across different types. ```python trait Emailable: fn get_email(self) -> String: ... struct User(Emailable): var username: String fn __init__(inout self, name: String): self.username = name fn get_email(self) -> String: return self.username + "@example.com" ``` The `inout` keyword signifies a mutable reference, while `owned` transfers ownership to the function, and `borrowed` (the default) allows read-only access. These keywords give you granular control over memory without a heavy garbage collector. Practical Migration Strategy Transitioning codebases doesn't require a total rewrite. You can call Python functions within Mojo by wrapping them in `try/except` blocks to handle the potential errors inherent in Python's dynamic nature. This allows you to migrate performance-critical modules to Mojo incrementally while keeping the rest of your ecosystem intact.
Feb 2, 2024Overview: Why Rust Matters for Python Developers Rust is rapidly becoming a favorite for developers who need high-performance code without sacrificing safety. For those coming from Python, Rust offers a solution to the traditional trade-off between speed and developer ergonomics. While Python excels at rapid prototyping and high-level abstractions, it often hits performance ceilings. Rust allows you to write performance-critical modules that can be bound directly to Python, effectively giving you the best of both worlds: Python's ease of use and Rust's bare-metal execution speed. Prerequisites To get the most out of this transition, you should have a solid grasp of Python's object-oriented concepts like classes and Abstract Base Classes (ABCs). Familiarity with basic terminal commands is necessary, as Rust requires a compilation step before execution. You will need the Rust compiler installed on your system to follow along with the code examples. Key Libraries & Tools - **rustc**: The primary compiler for the Rust language. - **PyO3**: A critical library for creating Rust bindings for Python, allowing seamless integration. - **Cargo**: Rust’s build tool and package manager (essential for managing project dependencies). Code Walkthrough: From Classes to Structs In Python, you encapsulate data and behavior in a class. Rust separates these concerns. Data lives in a `struct`, while behavior is defined in an `impl` (implementation) block. ```rust struct User { name: String, email: String, } impl User { fn new(name: &str) -> User { User { name: name.to_string(), email: format!("{}@example.com", name), } } } ``` In this snippet, the `struct` defines the shape of our data. The `impl` block contains the `new` function, which acts like a constructor. Notice the `format!` macro; the exclamation mark indicates it is a macro, which expands at compile time to handle a variable number of arguments—a feature standard Rust functions do not support. Syntax Notes: Ownership and Mutability Rust’s strictest rule is that variables are immutable by default. In Python, you can reassign attributes at will. In Rust, you must explicitly use the `mut` keyword to allow changes. ```rust let mut user = User::new("Arjan"); user.name = String::from("ArjanCodes"); ``` Without `mut`, the compiler will throw an error. This design prevents a massive class of bugs related to shared state and unintended side effects, forcing you to think about the lifecycle of your data from the start. Practical Examples: Handling Errors Without Exceptions Python relies on `try/except` blocks. Rust uses the monadic error handling pattern via the `Result` and `Option` types. This approach forces you to handle the possibility of failure explicitly using pattern matching. ```rust match get_user_result(name) { Ok(user) => println!("User found: {}", user.name), Err(e) => println!("Error: {}", e), } ``` This ensures that your program cannot ignore an error state, making the code significantly more robust than traditional exception-based logic. Tips & Gotchas One common pitfall for Pythonistas is the "borrow checker." Rust tracks who owns a piece of memory and when it can be deleted. While Python’s garbage collector handles this automatically, Rust requires you to be explicit about references (`&`). If you try to use data after it has been moved, the compiler will stop you. Embrace the compiler’s errors; they are not failures but a guide to writing safer, faster software.
Dec 29, 2023The Trap of the Academic Ego Many developers believe that a wall of certificates and a high-ranking title guarantee success. During my tenure as an Associate Professor, I saw how academia breeds a culture of individual ego. The focus shifts from solving societal problems to expanding personal networks and authoring papers just to see your name in print. This mindset is toxic for software development. In the real world, a user doesn't care about your PhD; they care if your code solves their problem. Breaking free from this prestige-driven bubble was the first step toward building something that actually mattered. Solving the Wrong Problems with Great Tech In my first startup, I made the classic blunder: building a solution in search of a problem. We created a website builder for musicians, followed by a "LinkedIn for choirs." Technically, the second iteration was brilliant. We used React and wrote clean, modular code. However, we ignored the market. Our target users preferred door-to-door sales over digital ticketing. You can write the most elegant Python scripts in the world, but if the product doesn't serve a customer need, the company will fail. Technology is a tool, not the destination. The Software Design Silver Lining Every failed pivot forced me to rebuild systems under extreme pressure. This constant cycle of "failing fast" turned into an accidental masterclass in Software Design. I started coding with the assumption that everything would change in thirty days. This forced me to adopt flexible architectures and best practices that I never would have mastered in the slow-moving comfort of a university office. My current success with ArjanCodes didn't happen despite these failures; it happened because of them. A Mindset Shift for the Modern Dev Failure isn't an absolute state unless you stop moving. If you extract a lesson, the experience becomes an investment. Don't fear the messy career path. Those broken builds and failed product launches are the very things that will eventually make your expertise unique and valuable.
Jul 7, 2023Overview Packaging Python code transforms a collection of scripts into a professional, reusable library. This process allows developers to share binary files and source code via a package manager, ensuring dependencies remain managed and installation stays consistent across different environments. By publishing to the PyPI repository, you enable others to install your work with a simple command, moving beyond manual file sharing to a scalable distribution model. Prerequisites To follow this guide, you should have a solid grasp of Python fundamentals and familiarity with the command line. You must have Python installed (version 3.10 or newer is recommended). Additionally, you will need a PyPI account for the final publishing step and a basic understanding of directory structures, specifically how `__init__.py` files designate packages. Key Libraries & Tools * **setuptools**: The standard library for building and distributing Python packages. * **wheel**: A tool that creates a binary distribution format, making installation faster than source distributions. * **twine**: A utility used to securely upload your packaged distributions to PyPI. * **PyPI**: The official third-party software repository for Python. Code Walkthrough The heart of the process lies in the `setup.py` file. This script uses setuptools to define your package's metadata and dependencies. ```python from setuptools import setup, find_packages with open("README.md", "r") as f: long_description = f.read() setup( name="id_generator", version="0.1.0", description="A library for generating various IDs", long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(where="app"), package_dir={"": "app"}, install_requires=["bson"], extras_require={"dev": ["pytest", "twine"]}, python_requires=">=3.10", classifiers=[ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", ], ) ``` In this snippet, `find_packages` automatically locates the source code in the `app` directory. The `install_requires` list ensures that bson installs automatically whenever a user downloads your package. We use `long_description` to pull in the README, ensuring the project page on PyPI looks professional. To build the package, run the following commands in your terminal: ```bash Create the binary (wheel) and source distribution (sdist) python setup.py bdist_wheel sdist Check the build for errors twine check dist/* ``` Syntax Notes Python packaging uses specific conventions for folder structures. Your source code should reside in a subfolder (like `app/` or `src/`) to prevent the build tools from accidentally including test files or configuration scripts in the final distribution. Always include a `LICENSE` file and an `__init__.py` in your package directory to signify that the folder is an importable Python package. Practical Examples Packaging is indispensable for internal company tools or open-source utilities. For instance, if you build a custom ID generator used by both a front-end Django app and a back-end data processing script, packaging the generator allows both projects to track it as a versioned dependency. This prevents "code rot" where different parts of a system use different versions of the same logic. Tips & Gotchas Avoid the trap of publishing directly to the live PyPI server immediately. Use the TestPyPI repository first. It acts as a sandbox, allowing you to verify that your README renders correctly and the installation works without cluttering the official registry. Also, ensure you bump your version number in `setup.py` before every upload; PyPI will reject any file that reuses an existing version string.
Mar 3, 2023Overview FastAPI represents a modern shift in Python web development. It streamlines the creation of RESTful APIs by prioritizing speed, developer productivity, and safety. Unlike older frameworks, it handles the heavy lifting of JSON serialization and data validation automatically. This allows developers to focus on business logic rather than boilerplate code. By leveraging standard Python type hints, it creates a robust contract between the client and the server, ensuring data integrity at every entry point. Prerequisites To follow this guide, you should have a baseline understanding of Python 3.6+ and basic object-oriented programming. Familiarity with decorators, type hinting, and the HTTP protocol (Verbs like GET, POST, PUT, DELETE) is essential. You should also be comfortable using a terminal to execute shell commands and manage virtual environments. Key Libraries & Tools * **FastAPI**: The core web framework used for building the API routes and handling logic. * **Uvicorn**: An ASGI server implementation used to run the application. * **Pydantic**: A library used for data validation and settings management via Python type annotations. * **Gunicorn**: A production-grade WSGI HTTP Server that can manage multiple worker processes for better scaling. Code Walkthrough 1. Installation and Setup Begin by installing the necessary packages. Using the `[standard]` flag with Uvicorn ensures you have all the recommended dependencies for performance. ```bash pip install "fastapi[all]" Or install selectively pip install fastapi uvicorn[standard] ``` 2. Basic Application Structure Initialize the application and define your data models using Pydantic. This setup defines an inventory system with an `Item` class. ```python from fastapi import FastAPI from pydantic import BaseModel from enum import Enum app = FastAPI() class Category(Enum): TOOLS = "tools" CONSUMABLES = "consumables" class Item(BaseModel): name: str price: float count: int id: int category: Category items = {0: Item(name="Hammer", price=9.99, count=20, id=0, category=Category.TOOLS)} ``` 3. Handling Parameters and Logic FastAPI differentiates between path parameters (in the URL) and query parameters (appended after a `?`). It also manages error handling via `HTTPException`. ```python from fastapi import HTTPException @app.get("/items/{item_id}") def get_item(item_id: int): if item_id not in items: raise HTTPException(status_code=404, detail="Item not found") return items[item_id] ``` 4. Running the Server Launch the API using the terminal. The `--reload` flag is vital for development as it refreshes the server whenever you save changes. ```bash uvicorn main:app --reload ``` Syntax Notes FastAPI uses decorators like `@app.get()` to bind functions to specific URL paths and HTTP methods. The framework relies heavily on Python's type hints (`item_id: int`). If a user passes a string where an integer is expected, the framework intercepts the request and returns a 422 Unprocessable Entity error automatically. This type-driven approach serves as the foundation for both validation and the auto-generated documentation. Practical Examples Real-world applications for this framework include building microservices for e-commerce inventory, creating backends for mobile applications, or developing machine learning model endpoints where input data must be strictly validated before processing. Tips & Gotchas * **Validation Constraints**: Use `Path` and `Query` from `fastapi` to add constraints like `gt=0` (greater than zero) or `max_length=8` directly in the function arguments. * **Scaling**: While Uvicorn is great for development, use Gunicorn with Uvicorn workers in production to handle concurrent requests effectively. * **Documentation**: Access `/docs` or `/redoc` on your running server to see interactive documentation generated in real-time.
Feb 10, 2023Overview of Data-Oriented Programming Standard Python classes often focus on behavior, exposing methods like `process_payment()` or `handle_click()`. However, much of our work involves simply moving and storing information. Python introduced data classes to streamline these data-oriented structures. Instead of manually writing boilerplate for object comparison, string representation, and initialization, the `@dataclass` decorator automates these tasks. This allows you to focus on the data structure itself rather than the mechanics of the class machinery. Prerequisites and Essentials To follow this guide, you should have a solid grasp of Python 3.7 or newer, though many features discussed here require Python 3.10. You should understand basic class definitions, type hinting, and the concept of dunder (double underscore) methods. Key Libraries & Tools * **dataclasses**: A standard library module providing the `@dataclass` decorator and the `field()` function for advanced attribute configuration. * **typing**: Used for type hinting, specifically for `List` or `Union` structures. * **timeit**: A utility for benchmarking code performance, specifically to test execution speed improvements. Code Walkthrough: From Boilerplate to Data Class Consider a basic `Person` class. Without data classes, printing an object results in a useless memory address. You would have to write a custom `__init__` and `__str__` method. By applying the decorator, the code shrinks significantly. ```python from dataclasses import dataclass, field @dataclass class Person: name: str address: str active: bool = True email_addresses: list[str] = field(default_factory=list) ``` In this snippet, the decorator handles the constructor and string representation. Note the use of `default_factory` for the list; setting a default value to `[]` directly would share the same list across all instances, causing significant bugs. Handling Advanced Initialization Sometimes you need logic that occurs after initialization. The `__post_init__` method allows you to generate values based on other fields. By setting `init=False` in a field, you ensure the user cannot provide that value manually during object creation. ```python @dataclass class Person: name: str address: str _search_string: str = field(init=False, repr=False) def __post_init__(self): self._search_string = f"{self.name} {self.address}" ``` Syntax Notes and 3.10 Features Python 3.10 introduced several game-changing arguments for the decorator. Setting `kw_only=True` forces users to specify argument names, preventing accidental value swaps. `match_args` enables structural pattern matching support, which is active by default. Perhaps the most impactful addition is `slots=True`. Standard classes use a dictionary (`__dict__`) to store attributes, which is flexible but slow. Slots use a more direct memory layout, significantly increasing access speeds. Performance and Practical Examples Using `slots=True` can result in a performance boost of over 20% during attribute access and deletion. This is vital for data-heavy applications like financial modeling or large-scale data processing where thousands of objects are instantiated and modified frequently. However, slots come with a trade-off: they do not support multiple inheritance easily, which can cause conflicts if base classes also use slots. Tips and Common Gotchas One frequent mistake is forgetting the decorator entirely. If you define variables in a class without `@dataclass`, Python treats them as class variables, shared by all instances, rather than instance variables. Additionally, use `frozen=True` whenever possible. Making your data structures immutable prevents side effects and makes your code much easier to debug and reason about.
Mar 25, 2022Overview Functions in Python are far more than mere containers for reusable code blocks. They are first-class citizens, meaning you can pass them as arguments, return them from other functions, and assign them to variables. This guide explores how to refactor a traditional, class-heavy Strategy Pattern into a leaner, functional architecture using Closures and Partial Function Application. By moving away from rigid class structures, you can reduce boilerplate and increase the flexibility of your software design. Prerequisites To follow this tutorial, you should have a solid grasp of basic Python syntax and the fundamentals of Object-Oriented Programming (OOP). Familiarity with Type Hinting and the concept of interfaces or abstract base classes will help you understand the refactoring process from classes to functions. Key Libraries & Tools * **typing**: Specifically the `Callable` and `List` types to define function signatures. * **functools**: Specifically the `partial` utility, which enables the creation of new functions from existing ones with pre-filled arguments. Code Walkthrough 1. Defining the Function Type Instead of a protocol class, we define a clear type alias using `Callable`. This ensures any strategy function we write takes a list of integers (prices) and returns a boolean. ```python from typing import Callable, List TradingStrategy = Callable[[List[int]], bool] ``` 2. The Closure Approach Closures allow us to "bake" configuration into a function. By wrapping our strategy logic inside an outer function, the inner function retains access to the outer variables even after the outer function has finished execution. ```python def should_buy_average_closure(window_size: int) -> TradingStrategy: def should_buy(prices: List[int]) -> bool: # Accesses window_size from the outer scope return sum(prices[-window_size:]) / window_size > prices[-1] return should_buy ``` 3. Partial Function Application While closures work, they are verbose. functools.partial offers a cleaner alternative. It allows you to take a function that requires multiple arguments and return a new function with some of those arguments already set. ```python from functools import partial def should_sell_minmax(prices: List[int], max_price: int) -> bool: return prices[-1] > max_price Create a specific strategy with a pre-set max_price sell_strategy = partial(should_sell_minmax, max_price=35000) ``` Syntax Notes Python allows the use of underscores in numeric literals (e.g., `35_000`) to improve readability. This has no impact on the value but makes it significantly easier to count zeros in high-frequency trading contexts. Additionally, when using `Callable`, the first argument is a list of input types, and the second is the return type. Practical Examples These techniques are ideal for trading bots where you need to swap out logic frequently. You can mix and match a "Buy" strategy from an average-based model with a "Sell" strategy from a min-max model without creating complex class hierarchies. This also applies to data processing pipelines where transformation steps need specific configurations. Tips & Gotchas Avoid confusing Currying with partial application. Currying breaks a multi-argument function into a chain of single-argument functions, whereas partial application simply fixes a few arguments at once. Always use type hints with your partial functions to ensure the calling code remains aware of the expected input types.
Mar 18, 2022Overview Refactoring isn't just about making code look pretty; it's about making it maintainable and clear. In this second phase of our Battleship refactor, we focus on moving beyond basic input handling to address the structural decay that often plagues growing projects. By separating the board's internal representation from the game's orchestration logic, we create a system where each component has one job. This approach eliminates the "God Class" problem where a single object knows too much about everything, making the game easier to scale, test, and debug. Prerequisites To get the most out of this tutorial, you should be comfortable with Python basics, specifically classes and methods. You should understand the difference between instance variables and local variables, and have a passing familiarity with list comprehensions and the modulo operator. Key Libraries & Tools * **Python 3.x**: The core programming language used for the implementation. * **Random Module**: A standard Python library used to generate the hidden ship's coordinates. * **List Comprehensions**: A concise Python syntax used for initializing the game grid efficiently. Moving Magic Numbers to Constants Hardcoded values, or "magic numbers," are a maintenance nightmare. If you decide to change your board from 5x5 to 10x10, you don't want to hunt through twenty lines of code to update every instance of the number five. We start by defining clear constants at the top of our module. ```python GUESSES_COUNT = 5 BOARD_SIZE_X = 5 BOARD_SIZE_Y = 5 HIDDEN = "O" SHIP = "S" GUESS = "X" ``` By using uppercase naming conventions, we signal to other developers that these values are configuration points, not variables that should change during runtime. This immediately makes the code more readable—seeing `SHIP` is far more descriptive than seeing a random string `"S"` buried in an `if` statement. Encapsulating the BattleshipBoard The original code mixed game rules with grid management. We solve this by creating a dedicated `BattleshipBoard` class. This class acts as the single source of truth for the game state. Instead of the game logic manually checking nested lists, it asks the board questions like "Is this a ship?" or "Has this spot been guessed?" ```python class BattleshipBoard: def __init__(self, size_x: int, size_y: int): self.grid = [[HIDDEN for _ in range(size_x)] for _ in range(size_y)] self.ship_row = random.randint(0, size_x - 1) self.ship_col = random.randint(0, size_y - 1) self.grid[self.ship_row][self.ship_col] = SHIP def is_ship(self, row: int, col: int) -> bool: return self.grid[row][col] == SHIP def to_string(self, show_ship: bool = False) -> str: rows_string = [] for row in self.grid: row_repr = [HIDDEN if col == SHIP and not show_ship else col for col in row] rows_string.append(" ".join(row_repr)) return "\n".join(rows_string) ``` The `to_string` method is a key design choice. By allowing a `show_ship` toggle, we separate the internal data (the actual grid) from the visual representation (what the player sees). This keeps the logic clean and avoids the need for maintaining two separate "visible" and "hidden" boards. Simplifying Game Logic with Math Managing player turns often involves messy index tracking. We can replace complex conditional blocks with simple math using the modulo operator (`%`). This allows us to rotate through any number of players seamlessly within a single `while` loop. ```python def play_game(player_count: int, board: BattleshipBoard): total_guesses = 0 max_guesses = GUESSES_COUNT * player_count while total_guesses < max_guesses: current_player = (total_guesses % player_count) + 1 # ... turn logic ... total_guesses += 1 ``` This math ensures that `current_player` always cycles correctly (1, 2, 1, 2 for a two-player game) without needing to manually reset a counter. It’s a robust pattern that reduces the surface area for bugs. Syntax Notes * **List Comprehensions**: Used `[[HIDDEN for _ in range(x)] for _ in range(y)]` to avoid reference copying issues that occur with simple list multiplication. * **Double Slash Division (`//`)**: Used for integer division to ensure guess counts remain whole numbers. * **Dunder Methods**: We avoided `__str__` in favor of `to_string` because we needed to pass the `show_ship` argument, which standard string conversion doesn't support. Tips & Gotchas Avoid the temptation to put print statements inside your core logic classes. Notice how `BattleshipBoard` returns strings but doesn't print them. This makes the class portable; you could theoretically use the same board class in a web app or a GUI without changing a line of code because it doesn't care how its data is displayed.
Mar 4, 2022