MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

The ECS Spot Instance Dilemma: When Task Placement Strategies Force Impossible Trade-Offs

2026-01-14 00:44:32

The Operational Reality of Spot Instances

Spot instances offer compelling cost savings—often 60-70% compared to on-demand pricing. For organizations running containerized workloads, this translates to substantial infrastructure budget reductions. The business case is clear: migrate to spot instances wherever possible.

However, adopting spot instances introduces a challenging operational problem.

The Problem: Alarm Fatigue and Service Degradation

Spot instances terminate frequently—sometimes multiple times per day across a cluster. Each termination triggers cascading effects:

Monitoring alerts fire continuously:

  • CloudWatch alarms: "ECS service below desired task count"
  • Application metrics: Spike in 5xx errors during task replacement
  • Load balancer health checks: Temporary target unavailability
  • Cluster capacity warnings: "Instance terminated in availability-zone-a"

Customer-facing impact:

  • External monitoring (Pingdom, Datadog) detects brief service degradation
  • 5xx error rates spike for 30-90 seconds during task rescheduling
  • Response times increase while remaining tasks handle full load
  • On-call engineers receive pages for incidents that "self-heal" within minutes

The irony: services recover automatically through ECS's built-in resilience mechanisms, but not before generating alerts, incident tickets, and potential customer complaints.

The Obvious Solution Has an Expensive Catch (For Small Clusters)

The standard recommendation for spot resilience is straightforward: spread tasks across multiple instances using ECS placement strategies.

{
  "placementStrategy": [
    {"type": "spread", "field": "instanceId"}
  ]
}

This configuration ensures that losing one instance affects only a small percentage of total capacity. The blast radius becomes manageable.

The problem: This approach works well at scale but becomes prohibitively expensive for small-to-medium services and clusters.

Large service (100+ tasks):

  • 100 tasks spread across 15-20 instances
  • Each instance: 5-7 tasks (50-70% utilization)
  • Spread strategy achieves good distribution AND efficient resource usage
  • Problem minimal: Tasks naturally fill available capacity

Small-to-medium service (5-20 tasks):

  • 10 tasks spread across 10 instances
  • Each instance: 1 task (10-20% utilization)
  • Spread strategy forces massive over-provisioning
  • Problem severe: 80-90% of resources wasted

Note: In practice, small services typically run in small clusters (one or a few services per cluster), so "small service" and "small cluster" often refer to the same deployment pattern.

The cost impact:

  • Spot savings: 60% reduction = $400/month saved
  • Over-provisioning penalty: 8 idle instances = $600/month wasted
  • Net result: Higher costs than running on-demand without spot instances

Organizations running small-to-medium clusters (the majority of microservices deployments) face a dilemma:

  • Option A: Accept frequent alarms and occasional customer-facing incidents (operational burden)
  • Option B: Over-provision instances for resilience (eliminates cost savings)
  • Option C: Revert to on-demand instances (forfeit 60% savings opportunity)

None of these options are satisfactory for small-to-medium workloads. Let's analyze this technical challenge in detail and explore how different orchestration platforms handle this scale-dependent problem.

The "Impossible Triangle" (For Small-to-Medium Clusters)

This operational challenge can be visualized as an optimization problem with three competing objectives:

       Spot Resilience
    (minimize alarm fatigue
     & customer impact)
            /\
           /  \
          /    \
         /      \
        /        \
       /          \
      /____________\
Cost           Auto-Scaling
Efficiency     (5-20 tasks)

Challenge: Optimize for all three simultaneously

Important context: This problem is scale-dependent. Large services (50+ tasks) naturally solve this triangle—enough tasks to both spread across instances AND utilize resources efficiently. The dilemma is specific to small-to-medium clusters where individual services have 5-20 tasks, representing the majority of modern microservice deployments.

In practice, organizations discover that container orchestration platforms force trade-offs between these objectives for smaller services. Achieving all three requires either platform-specific workarounds or architectural capabilities that some platforms simply don't provide.

AWS ECS: Exploring Placement Strategies

Approach 1: Maximum Spread Strategy (Solves Alarms, Destroys Budget)

The most straightforward approach to eliminating alarm fatigue is maximizing task distribution:

{
  "serviceName": "api-service",
  "desiredCount": 10,
  "capacityProviderStrategy": [{
    "capacityProvider": "spot-asg-provider",
    "weight": 1
  }],
  "placementStrategy": [
    {
      "type": "spread",
      "field": "instanceId"
    }
  ]
}

Behavior:

  • ECS places 1 task per instance (maximum distribution)
  • Capacity Provider provisions 10 instances for 10 tasks
  • Each instance: ~10-20% resource utilization
  • Cost: $250/month (10 × m5.large spots @ $25/month)

Operational impact:

  • ✅ Spot termination affects only 1 task (10% capacity loss)
  • ✅ No Pingdom alerts: Service handles loss gracefully
  • ✅ Minimal 5xx error spikes: 90% of capacity remains available
  • ✅ CloudWatch alarms stay quiet: Task replacement happens within normal thresholds

Cost impact:

  • ❌ Resource utilization: 10-20% per instance (80-90% waste)
  • ❌ Over-provisioning: 8-9 instances running mostly idle
  • ❌ Scale-down lag: ASG retains instances during low-demand periods
  • Net cost higher than on-demand baseline

The paradox: This configuration solves the operational problem (no alarms, no incidents) but negates the entire financial justification for using spot instances in the first place.

Approach 2: Binpack Strategy (Saves Money, Triggers Alarms)

To reclaim cost efficiency, the next approach focuses on resource utilization:

{
  "placementStrategy": [
    {
      "type": "spread",
      "field": "attribute:ecs.availability-zone"
    },
    {
      "type": "binpack",
      "field": "memory"
    }
  ]
}

Behavior:

  • ECS spreads across availability zones, then binpacks within each zone
  • Capacity Provider provisions 3 instances for 10 tasks
  • Each instance: 70-80% resource utilization
  • Cost: $75/month (3 × $25/month)

Task distribution:

Instance 1 (spot): 4 tasks
Instance 2 (spot): 3 tasks
Instance 3 (spot): 3 tasks

Cost impact:

  • ✅ Resource utilization: 70-80% (efficient)
  • ✅ Spot savings realized: ~60% vs on-demand
  • ✅ Auto-scaling works: Capacity Provider adjusts instance count

Operational impact:

  • Spot termination blast radius: 30-40% capacity loss
  • ❌ Pingdom alerts fire: 5xx error rate spikes above threshold
  • ❌ CloudWatch alarms trigger: "Service degraded - insufficient healthy tasks"
  • ❌ Recovery lag: 3-5 minutes for new instance + task startup
  • ❌ Customer complaints: Brief but noticeable service interruptions

The incident pattern: When Instance 1 terminates (daily occurrence), 4 tasks disappear simultaneously. Remaining 6 tasks handle 100% of traffic, causing:

  1. Response time degradation (overload)
  2. Connection timeouts (queue saturation)
  3. 5xx errors (backend unavailable)
  4. PagerDuty/on-call escalation

By the time engineers acknowledge the page, ECS has already recovered. But the alarm fatigue accumulates—multiple times per day, every day.

Approach 3: Capacity Provider targetCapacity

A common misconception is that targetCapacity controls task distribution:

{
  "capacityProvider": "my-asg-provider",
  "managedScaling": {
    "targetCapacity": 60
  }
}

Reality: targetCapacity determines the cluster utilization threshold for triggering scale-out, not how tasks are distributed across instances.

Behavior:

  • targetCapacity: 100 = Scale when cluster reaches 100% capacity
  • targetCapacity: 60 = Scale when cluster reaches 60% capacity (maintains 40% headroom)

With a binpack strategy, tasks still concentrate on fewer instances. Lower targetCapacity provisions more instances but doesn't change the distribution pattern—the additional instances remain underutilized.

Common ECS Workarounds

Workaround 1: Small Instance Types

Use instance types with limited capacity to physically constrain task density:

{
  "placementStrategy": [
    {"type": "spread", "field": "instanceId"}
  ]
}

// ASG Configuration
// Instance type: t4g.small (2GB RAM)
// Task memory requirement: 1GB
// Physical limit: 2 tasks per instance maximum

Outcome:

  • 10 tasks → 5 instances required (2 tasks each)
  • Cost: 5 × $5/month = $25/month
  • Blast radius: 20% (acceptable for most use cases)

Trade-off: This approach uses physical constraints as a proxy for scheduling policy, which feels architecturally inelegant.

Note: For small ECS clusters, this workaround effectively balances cost efficiency and spot protection. However, this raises a broader architectural question: should clusters use many small instances or fewer large instances? That debate involves considerations around bin-packing efficiency, operational overhead, blast radius philosophy, and AWS service limits—topics beyond the scope of this discussion. For the specific problem of spot resilience in small services, small instance types provide a pragmatic solution regardless of overall cluster architecture.

Workaround 2: Hybrid On-Demand + Spot

{
  "capacityProviderStrategy": [
    {
      "capacityProvider": "on-demand-provider",
      "base": 3,
      "weight": 0
    },
    {
      "capacityProvider": "spot-provider",
      "base": 0,
      "weight": 1
    }
  ]
}

Outcome:

  • First 3 tasks on on-demand instances (never terminated)
  • Tasks 4-10 on spot instances (cost-optimized)
  • Spot termination affects only 10-30% of capacity
  • Base capacity remains stable

Cost:

  • On-demand: 3 instances × $50/month = $150/month
  • Spot: 2-4 instances × $15/month = $30-60/month
  • Total: $180-210/month

Trade-off: Higher baseline cost for improved reliability.

Alternative: Kubernetes Addresses This Naturally

Other container orchestration platforms handle this problem differently. Kubernetes, for example, provides topologySpreadConstraints that directly specify the maximum number of pods per node:

spec:
  topologySpreadConstraints:
  - maxSkew: 2  # Max 2 pods per node
    topologyKey: kubernetes.io/hostname

This simple configuration achieves all three objectives for small-to-medium clusters:

  • Spot resilience: 20% blast radius (2 pods per node)
  • Cost efficiency: 5 nodes instead of 10 (50% reduction)
  • Auto-scaling: Cluster autoscaler adjusts node count dynamically

The maxSkew parameter provides granular control (1, 2, 5, etc.) over the distribution density, enabling precise optimization along the resilience-efficiency spectrum—something ECS placement strategies cannot express directly.

The Fundamental Architectural Difference

The core issue isn't ECS inadequacy—it's an architectural constraint for small-to-medium clusters:

ECS lacks granular per-instance task limits.

Available strategies:

  • spread by instanceId = Exactly 1 task per instance (maximum spread, works well for large services)
  • binpack = As many tasks as resources allow (maximum density)
  • spread by AZ + binpack = Zone distribution, then density (no per-instance control)

For small-to-medium clusters (5-20 tasks per service), these binary options force choosing between over-provisioning (spread) or excessive blast radius (binpack). There's no middle ground to specify "aim for 2-3 tasks per instance."

When ECS Remains the Better Choice

Despite these limitations, ECS is often the pragmatic choice when:

  1. Large-scale deployments: Services running 50+ tasks naturally achieve efficient distribution with spread strategies
  2. Simple placement requirements: Consistent task count, no spot instances, availability zone distribution sufficient
  3. Deep AWS integration needed: Native IAM roles, ALB/NLB integration, CloudWatch, ECS Exec
  4. Team expertise: Existing operational knowledge, established runbooks, monitoring dashboards
  5. Fargate deployment: Serverless container management without EC2 instance overhead
  6. Managed control plane: No cluster version management, automatic scaling, maintenance-free

Critical insight: The "impossible triangle" primarily affects small-to-medium clusters (5-20 tasks per service). At larger scales (50+ tasks per service), spread strategies achieve both good distribution and efficient resource usage simultaneously. ECS's simpler model reduces operational complexity for straightforward use cases and scales excellently for high-volume services.

Key Takeaways

  1. Scale-Dependent Problem: The "impossible triangle" primarily affects small-to-medium clusters (5-20 tasks per service). Large services (50+ tasks) naturally achieve both good distribution and efficient resource usage.

  2. Root Cause: ECS lacks granular per-instance task limits—only extreme options exist (1 task/instance spread OR full binpack), with no middle ground.

  3. Practical Workarounds: Small instance types (t4g.small) provide the most effective solution, physically limiting task density while maintaining cost efficiency ($25/month vs $250/month).

  4. Platform Limitations: Other orchestration platforms provide granular controls that directly address this problem, highlighting an architectural constraint rather than a configuration issue.

Conclusion

The spot instance adoption dilemma reveals a fundamental constraint in ECS's task placement architecture: the absence of granular per-instance task limits.

The scale-dependent reality: For large-scale services (50+ tasks), ECS placement strategies work excellently—tasks naturally distribute across instances while maintaining efficient resource utilization. The "impossible triangle" problem emerges specifically for small-to-medium clusters (5-20 tasks per service) that dominate modern microservice architectures.

For these smaller clusters:

  • Spread strategy eliminates alarms but destroys cost efficiency
  • Binpack strategy saves money but triggers constant operational incidents
  • Workarounds exist (small instances, hybrid capacity) but add complexity
  • Organizations ultimately choose: accept alarm fatigue OR forfeit spot savings

The broader lesson: Container orchestration platforms make architectural trade-offs that favor certain workload profiles. ECS's binary placement options (spread vs binpack) scale well at the extremes—either very large services or services where cost takes priority over operational stability.

Understanding these platform constraints enables realistic expectations and informed architectural decisions. When evaluating ECS for spot instance deployments, the critical question becomes: Does your cluster size align with where ECS placement strategies excel?

For small-to-medium clusters, the operational pain of alarm fatigue may ultimately outweigh the promised cost savings—making the spot instance business case less compelling than it initially appears.

Running ECS on spot instances? Struggling with alarm fatigue or over-provisioning? Share your experiences and workarounds in the comments.

Further Reading:

Connect with me on LinkedIn: https://www.linkedin.com/in/rex-zhen-b8b06632/

I share insights on cloud architecture, container orchestration, and SRE practices. Let's connect and learn together!

The Cursor way to launch

2026-01-14 00:42:01

Last month, Cursor launched for the fifth time on Product Hunt in 2025.

The 2024 Product of the Year still hits the charts. They have launched web and mobile agents, a visual editor, and 2.0, consistently ranking in the Top 5 Products of the Day.

I had a look at their recent launches. Here's what I found.

What Cursor did right

  1. Straightforward tagline. The 60-character tagline might be the most important part of a launch. It's the first thing you see on the front page. What makes Cursor stand out? They highlight the features, not the benefits.

  2. Minimalist visual assets. The image gallery is the first impression of your product. It sets expectations. Cursor's recent launches highlighted 2 to 4 images. No stock images, no marketing fluff, just product screenshots. They show the product.

  3. Found a Hunter. Ben Lang was the hunter of their latest launches - an established user with 50K+ followers, among the 2022 Community Members of the Year (runner-up). It might help increase their reach.

  4. Feedback first. Like the tagline and visual assets, the first comment is the simplest. No looooooong background stories, they're just genuinely curious about what the community thinks of the release.

  5. Riding the tailwinds. Last but not least, the launch timing. They first announced the product updates on X, gained momentum, and then launched on Product Hunt the next day, riding the tailwinds.

Final thoughts

For context, Cursor has 20K+ followers on Product Hunt. It certainly helps get more exposure. However, IMHO it's only part of the formula. Straightforward tagline, minimalist image gallery, simple comments... It all resonates with developers, their target audience. To quote Lee Robinson on what great developer marketing looks like:

Great marketing values your time; every word matters. Show them how to build interesting things; don’t fill a post with 1,000 words of garbage.

-- Lee Robinson (Vercel, Cursor)

Developers are resistant to anything that looks, sounds, or smells like marketing. Cursor keeps it simple, and it works.

How to apply this to your launch

  • Keep the tagline relatable to your audience
  • Show the product in your image gallery
  • Find a Hunter
  • Engage with the community thoughtfully
  • The right time to launch could be now

Over to you! What are your key learnings from your previous product launches? What worked, what didn't work from your perspective?

Devlog #01: Making Fish Swim

2026-01-14 00:31:42

The last couple of months I've been fiddling around with Godot and did all sorts of random experiments, from educating myself and getting more comfortable with the engine, to building small prototypes, where I would challenge myself to build one specific thing first.

This journey taught me a lot about the engine and how I can utilize it in the future to build my first fully-fledged game.

Finding the game idea

I had a few ideas in mind what I wanted to create. Started with a heavy logical game with a lot of math and variables, as I wanted my first game to be a simulation game. I must admit, this was not the smartest idea for my first game but one, I'll definitely look into more in the future - not now.

In the end it came down to a game, where the player owns a fish tank, which they have to manage. It's more of a cozy idling management game, where the player feed fish, get more fish, get some upgrades to make life easier (autofeeder, cleaner, etc.), and maybe some challenges to keep the player actually engaged. The game plan is simple and a good starting point for my first game.

Iteration 1: Getting fish moving

From my previous experiments I knew how to move a character around on a 2D scene. So I started by searching for a placeholder fish sprite, placed it in my scene and added a simple movement script to it, which moved the fish around at random directions every few seconds. I used a timer that counts down using the delta value passed to Godot's _physics_process function.

The fish extends the CharacterBody2D node, which gives us built-in physics and collision handling. I used a simple state machine (enum State) to manage different behaviors, though for now we're only implementing the WANDER state.

class_name Fish
extends CharacterBody2D

enum State {WANDER, HUNGRY, EATING, FLEEING}

@onready var pathing: Line2D = $Pathing

@export var speed: float = 100.0
@export var prediction_length: float = 200.0
@export var prediction_steps: int = 20

var current_state: State = State.WANDER
var wander_direction: Vector2 = Vector2.ZERO
var wander_timer: float = 0.0

func _ready() -> void:
  _choose_new_wander_direction()

  if pathing != null:
    pathing.visible = OS.is_debug_build()

func _physics_process(delta: float) -> void:
  match current_state:
    State.WANDER:
      _handle_wander(delta)

  _update_visuals()

  if OS.is_debug_build():
    _update_path_prediction()

  move_and_slide()

func _handle_wander(delta: float) -> void:
  velocity = wander_direction * speed

  wander_timer -= delta
  if wander_timer <= 0.0:
    _choose_new_wander_direction()

  _keep_in_bounds()

func _choose_new_wander_direction() -> void:
  wander_direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
  wander_timer = randf_range(2.0, 5.0)

func _keep_in_bounds() -> void:
  var viewport_size: Vector2 = get_viewport().get_visible_rect().size
  var margin := 100.0
  var turn_force := 2.0

  if global_position.x < margin:
    wander_direction.x += turn_force * get_physics_process_delta_time()
  elif global_position.x > viewport_size.x - margin:
    wander_direction.x -= turn_force * get_physics_process_delta_time()

  if global_position.y < margin:
    wander_direction.y += turn_force * get_physics_process_delta_time()
  elif global_position.y > viewport_size.y - margin:
    wander_direction.y -= turn_force * get_physics_process_delta_time()

  wander_direction = wander_direction.normalized()

func _update_path_prediction() -> void:
  if not OS.is_debug_build() or not pathing:
    return

  if current_state != State.WANDER:
    pathing.clear_points()
    return

  pathing.clear_points()

  var sim_pos := Vector2.ZERO
  var sim_direction := wander_direction
  var step_distance := prediction_length / prediction_steps

  for i in prediction_steps:
    pathing.add_point(sim_pos)
    sim_pos += sim_direction * step_distance
    _simulate_boundary_steering(sim_pos, sim_direction)

func _simulate_boundary_steering(sim_pos: Vector2, sim_direction: Vector2) -> void:
  var viewport_size := get_viewport_rect().size
  var margin := 100.0
  var turn_force := 0.5

  var world_pos := global_position + sim_pos

  if world_pos.x < margin:
    sim_direction.x += turn_force
  elif world_pos.x > viewport_size.x - margin:
    sim_direction.x -= turn_force

  if world_pos.y < margin:
    sim_direction.y += turn_force
  elif world_pos.y > viewport_size.y - margin:
    sim_direction.y -= turn_force

  sim_direction = sim_direction.normalized()

func _update_visuals() -> void:
  if velocity.x != 0:
    $Sprite.flip_h = velocity.x < 0

_keep_in_bounds() applies a gentle steering force, when fish approach the edges. This prevents them from leaving the viewport. The _update_path_prediction() function is used to visualize the fish's predicted path, which helps debug the movement.

This looked good at first and fish actually moved around the scene, but Fish would just get stuck and bump into each other not respecting the boundaries of the scene nor each other.

I then decided to add some sort of avoidance behavior and had to tinker around on the script to actually get something "working". I added seperation behavior - when the fish get too close, they calculate a "push away" vector inversely proportional to their distance.

class_name Fish
extends CharacterBody2D

enum State {WANDER, HUNGRY, EATING, FLEEING}

@onready var pathing: Line2D = $Pathing

@export var speed: float = 100.0
@export var prediction_length: float = 200.0
@export var prediction_steps: int = 20
@export var avoidance_radius: float = 60.0
@export var avoidance_strength: float = 3
@export var steering_speed: float = 2.0

var current_state: State = State.WANDER
var wander_direction: Vector2 = Vector2.ZERO
var wander_timer: float = 0.0
var current_direction: Vector2 = Vector2.ZERO

func _ready() -> void:
  add_to_group("Fish")
  _choose_new_wander_direction()
  current_direction = wander_direction

  if pathing:
    pathing.visible = OS.is_debug_build()

func _physics_process(delta: float) -> void:
  match current_state:
    State.WANDER:
      _handle_wander(delta)

  _update_visuals()

  if OS.is_debug_build():
    _update_path_prediction()

  move_and_slide()

func _handle_wander(delta: float) -> void:
  var avoidance_direction := _avoid_other_fish()
  var direction: Vector2

  if avoidance_direction.length() > 0.1:
    direction = avoidance_direction
  else:
    direction = wander_direction

  current_direction = current_direction.lerp(direction, steering_speed * delta).normalized()

  if current_direction.length_squared() > 0.01:
    current_direction = current_direction.normalized()

  velocity = current_direction * speed

  wander_timer -= delta
  if wander_timer <= 0.0:
    _choose_new_wander_direction()

  _keep_in_bounds()

func _choose_new_wander_direction() -> void:
  wander_direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
  wander_timer = randf_range(2.0, 5.0)

func _avoid_other_fish() -> Vector2:
  var seperation := Vector2.ZERO
  var nearby_fish := 0

  var all_fish := get_tree().get_nodes_in_group("Fish")

  for fish in all_fish:
    if fish == self:
      continue

    var distance := global_position.distance_to(fish.global_position)

    if distance < avoidance_radius and distance > 0:
      Log.info("Avoiding fish at distance: %s" % distance)
      var push_away := global_position.direction_to(fish.global_position) * -1
      var strength := 1.0 - (distance / avoidance_radius)
      seperation += push_away * strength
      nearby_fish += 1

  if nearby_fish > 0:
    seperation = seperation / nearby_fish
    seperation = seperation.normalized() * avoidance_strength

  return seperation

func _keep_in_bounds() -> void:
  var viewport_size: Vector2 = get_viewport().get_visible_rect().size
  var margin := 100.0
  var turn_force := 2.0

  if global_position.x < margin:
    wander_direction.x += turn_force * get_physics_process_delta_time()
  elif global_position.x > viewport_size.x - margin:
    wander_direction.x -= turn_force * get_physics_process_delta_time()

  if global_position.y < margin:
    wander_direction.y += turn_force * get_physics_process_delta_time()
  elif global_position.y > viewport_size.y - margin:
    wander_direction.y -= turn_force * get_physics_process_delta_time()

  wander_direction = wander_direction.normalized()

func _update_path_prediction() -> void:
  if not OS.is_debug_build() or not pathing:
    return

  if current_state != State.WANDER:
    pathing.clear_points()
    return

  pathing.clear_points()

  var sim_pos := Vector2.ZERO
  var sim_direction := wander_direction
  var step_distance := prediction_length / prediction_steps

  for i in prediction_steps:
    pathing.add_point(sim_pos)
    sim_pos += sim_direction * step_distance
    _simulate_boundary_steering(sim_pos, sim_direction)

func _simulate_boundary_steering(sim_pos: Vector2, sim_direction: Vector2) -> void:
  var viewport_size := get_viewport_rect().size
  var margin := 100.0
  var turn_force := 0.5

  var world_pos := global_position + sim_pos

  if world_pos.x < margin:
    sim_direction.x += turn_force
  elif world_pos.x > viewport_size.x - margin:
    sim_direction.x -= turn_force

  if world_pos.y < margin:
    sim_direction.y += turn_force
  elif world_pos.y > viewport_size.y - margin:
    sim_direction.y -= turn_force

  sim_direction = sim_direction.normalized()

func _update_visuals() -> void:
  if current_direction.x != 0:
    $Sprite.flip_h = current_direction.x < 0

This was a good improvement as the fish now tried avoiding each other. However, another issue occurred: Fish would get stuck in the turning "animation" when bumping into each other or were about to bump into each other. This made it look very funny at first - but it was a bug I certainly have to fix as this will get very frustrating to look at especially when a couple of fish don't know what to do anymore and are stuck in an infinite turning loop while they still get new coordinates to move to.

Iteration 2: Fixing the turning loop

To fix this issue, I had to rethink how the fish were avoiding each other. The problem was that the avoidance behavior was too strong and would override the wander direction completely, causing fish to get stuck in a loop when multiple fish were trying to avoid each other at the same time.

I decided to implement a smoother steering system that would blend the avoidance direction with the wander direction, rather than replacing it entirely. I also added an alignment behavior that would help fish move in similar directions when they're near each other, which would reduce the chances of them getting stuck in a turning loop.

Here I switched the CharacterBody2D to Area2D because I no longer needed physics-based collision resolution as I'm now handling movement manually and using the area's signals to detect nearby fish more efficiently than iterating through all fish every frame.

class_name Fish
extends Area2D

enum State {WANDER, HUNGRY, EATING, FLEEING}

@onready var pathing: Line2D = $Pathing

@export var speed: float = 100.0
@export var prediction_length: float = 200.0
@export var prediction_steps: int = 20
@export var avoidance_radius: float = 60.0
@export var avoidance_strength: float = 5.0
@export var steering_speed: float = 2.0
@export var alignment_strength: float = 0.5

var velocity: Vector2 = Vector2.ZERO
var current_state: State = State.WANDER
var wander_direction: Vector2 = Vector2.ZERO
var wander_timer: float = 0.0
var current_direction: Vector2 = Vector2.ZERO

var nearby_fish: Array[Fish] = []

func _ready() -> void:
  add_to_group("Fish")
  _choose_new_wander_direction()
  current_direction = wander_direction

  area_entered.connect(_on_nearby_fish_entered)
  area_exited.connect(_on_nearby_fish_exited)

  if pathing:
    pathing.visible = OS.is_debug_build()

func _physics_process(delta: float) -> void:
  match current_state:
    State.WANDER:
      _handle_wander(delta)

  _update_visuals()

  if OS.is_debug_build():
    _update_path_prediction()

  global_position += velocity * delta

func _handle_wander(delta: float) -> void:
  var avoidance := _avoid_other_fish()
  var alignment := _align_with_other_fish()

  var avoidance_magnitude := avoidance.length()
  var wander_weight: float = clamp(1.0 - avoidance_magnitude / avoidance_strength, 0.2, 1.0)
  var desired_direction := ((wander_direction * wander_weight) + avoidance + alignment).normalized()
  var steer_speed := steering_speed * (1.0 + avoidance_magnitude)
  current_direction = current_direction.lerp(desired_direction.normalized(), steer_speed * delta).normalized()

  velocity = current_direction * speed

  wander_timer -= delta
  if wander_timer <= 0.0:
    _choose_new_wander_direction()

  _keep_in_bounds()

func _choose_new_wander_direction() -> void:
  wander_direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
  wander_timer = randf_range(2.0, 5.0)

func _avoid_other_fish() -> Vector2:
  if nearby_fish.is_empty():
    return Vector2.ZERO

  var seperation := Vector2.ZERO

  for fish in nearby_fish:
    if not is_instance_valid(fish):
      continue

    var to_fish := fish.global_position - global_position
    var distance := to_fish.length()

    if distance < avoidance_radius and distance > 1.0:
      var push_away := -to_fish.normalized()
      var strength := 1.0 - (distance / avoidance_radius)
      seperation += push_away * strength

  Log.info("Fish %s avoidance vector: %s" % [name, seperation])
  return seperation.normalized() * avoidance_strength if seperation.length() > 0.1 else Vector2.ZERO

func _align_with_other_fish() -> Vector2:
  if nearby_fish.is_empty():
    return Vector2.ZERO

  var average_direction := Vector2.ZERO

  for fish in nearby_fish:
    if not is_instance_valid(fish):
      continue

    average_direction += fish.current_direction

  return average_direction.normalized() * alignment_strength if average_direction.length() > 0.01 else Vector2.ZERO

func _keep_in_bounds() -> void:
  var viewport_size: Vector2 = get_viewport().get_visible_rect().size
  var margin := 100.0
  var turn_force := 2.0

  if global_position.x < margin:
    wander_direction.x += turn_force * get_physics_process_delta_time()
  elif global_position.x > viewport_size.x - margin:
    wander_direction.x -= turn_force * get_physics_process_delta_time()

  if global_position.y < margin:
    wander_direction.y += turn_force * get_physics_process_delta_time()
  elif global_position.y > viewport_size.y - margin:
    wander_direction.y -= turn_force * get_physics_process_delta_time()

  wander_direction = wander_direction.normalized()

func _update_path_prediction() -> void:
  if not OS.is_debug_build() or not pathing:
    return

  if current_state != State.WANDER:
    pathing.clear_points()
    return

  pathing.clear_points()

  var sim_pos := Vector2.ZERO
  var sim_direction := wander_direction
  var step_distance := prediction_length / prediction_steps

  for i in prediction_steps:
    pathing.add_point(sim_pos)
    sim_pos += sim_direction * step_distance
    _simulate_boundary_steering(sim_pos, sim_direction)

func _simulate_boundary_steering(sim_pos: Vector2, sim_direction: Vector2) -> void:
  var viewport_size := get_viewport_rect().size
  var margin := 100.0
  var turn_force := 0.5

  var world_pos := global_position + sim_pos

  if world_pos.x < margin:
    sim_direction.x += turn_force
  elif world_pos.x > viewport_size.x - margin:
    sim_direction.x -= turn_force

  if world_pos.y < margin:
    sim_direction.y += turn_force
  elif world_pos.y > viewport_size.y - margin:
    sim_direction.y -= turn_force

  sim_direction = sim_direction.normalized()

func _update_visuals() -> void:
  if current_direction.x != 0:
    $Sprite.flip_h = current_direction.x < 0

func _on_nearby_fish_entered(area: Area2D) -> void:
  if area is Fish and area != self:
    nearby_fish.append(area)

func _on_nearby_fish_exited(area: Area2D) -> void:
  if area is Fish:
    nearby_fish.erase(area)

Iteration 3: Reading about Navigation Agents

I was still not happy with the movement of the fish. Some issues would still occur, like fish getting stuck in corners or not being able to move around the scene properly. I then watched a video on YouTube about someone implementing navigation agents in his game but for player movement avoidance. This got me thinking: if navigation agents could also work for fish movement.

I had to read through the docs about the NavigationAgent2D node and how to actually use it. Navigation agents are actually quite neat as they come with a lot of built-in functionality for pathfinding and avoidance of obstacles. The decision was made to try it out and see, whether it would work for the fish movement.

Adding the NavigationAgent2D node to the fish and setting up the navigation map was quite easy. All I had to do was to create a navigation mesh on the game scene and set it up as the navigation map for the fish. Then I had to actually rewrite 80% of my fish.gd script to make use of the navigation agent.

class_name Fish
extends CharacterBody2D

@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
@onready var energy_bar: ProgressBar = $EnergyBar

## Minimum speed of fish
@export var speed_min: float = 10.0
## Maximum speed of fish
@export var speed_max: float = 80.0

# Timer for when fish will pick a new wander target
var wander_timer: float = 0.0
# Current speed of fish
var current_speed: float = 0.0
# Current fish energy
var current_energy: float = 100.0:
  set(value):
    current_energy = clamp(value, 0, 100)
    if energy_bar:
      energy_bar.value = current_energy
      energy_bar.visible = current_energy < 50
# Timer for how long fish will conserve energy
var conserving_energy_timer: float = 0.0

func _ready() -> void:
  add_to_group("fish")
  current_speed = randf_range(speed_min, speed_max)
  current_energy = randf_range(50, 100)

  if energy_bar:
    energy_bar.value = current_energy

  await get_tree().physics_frame
  await get_tree().physics_frame

  nav_agent.set_velocity_forced(Vector2.ZERO)

func _on_nav_agent_velocity_computed(suggested_velocity: Vector2) -> void:
  velocity = suggested_velocity
  move_and_slide()

  if velocity.x != 0:
    $Sprite.flip_h = velocity.x < 0

To my surprise, the code got simpler - well mostly because I moved most of the code to my state machine. I also decided there, to tinker around with energy management and added a simple energy bar to the fish (for debugging purposes), which would decrease over time and when it reaches 0, the fish would sort of stop getting new coordinates and just sort of "rest" for some time until its energy is restored.

The wandering state now looks like this:

extends FishState

func enter(previous_state_path: String, data := {}) -> void:
  _new_wander_location()

func physics_update(_delta: float) -> void:
  fish.wander_timer -= _delta

  # Consume energy when moving, gain energy when not moving
  fish.current_energy -= _delta * 0.1 if fish.velocity.length() > 10 else 0.05

  if fish.current_energy < 10:
    transitioned.emit(CONSERVING_ENERGY)
    return

  if fish.wander_timer <= 0.0 or fish.nav_agent.is_navigation_finished():
    _new_wander_location()

  if not fish.nav_agent.is_navigation_finished():
    var next_pos = fish.nav_agent.get_next_path_position()
    var dir = (next_pos - fish.global_position).normalized()
    fish.nav_agent.set_velocity(dir * fish.current_speed)

func _new_wander_location() -> void:
  var vp_size: Vector2 = get_viewport().get_visible_rect().size
  var target_position: Vector2 = Vector2(
    randf_range(100, vp_size.x - 100),
    randf_range(100, vp_size.y - 100)
  )

  fish.nav_agent.target_position = target_position
  fish.wander_timer = randf_range(3.0, 6.0)
  fish.current_speed = randf_range(fish.speed_min, fish.speed_max)

I'm happy with the result and the fish movement as it is now much more natural and looks a lot better in general. My state machine also simplified a lot of things and made the code a lot more maintainable.

The approach for the state machine I followed was the Finite State Machine pattern by Nathan Lovato, which also covered a node based implementation - where I'm at right now.

Node based state machine implementation is quite neat and makes the code a lot more readable as each state is a separate node in the scene tree (for me the Fish scene) and a separate script. This makes it easier to understand and maintain the code for me. I also know it might be a bit overkill for a simple game like this, but I thought it's a good learning experience and I would definitely benefit from it in my future games.

My learnings from this

The project is still far from being finished but I'm happy with the overall progress I've made so far. I'm not working 24/7 on it but when I do, I feel like making progress, even though I'm somewhat slow at it. What I've learned in November was quite valuable and I'm looking forward to continue the journey on the game.

Some key takeaways from this journey:

  • Navigation agents handle avoidance better than manual steering
  • State machines make complex behaviors manageable
  • Starting simple and iterating is more effective than over-engineering upfront

What I plan to do next

With the movement of the fish sorted out, I'm actually now trying to tinker around with some UI elements and the game mechanics like the feeding and the cleaning of the tank and the entire buying update system - which would require to tinker around with a minimal economy system.

The project is open source and can be found on my GitHub profile. You can find the repository here. I'm also happy to receive any feedback or suggestions you might have, as I'm looking to actually improve my understanding of how to make games.

Okay, I think this is enough yapping for now. I'll definitely write more devlogs like these in the future. 👋

PYTHON FUNDAMENTALS | From Basics to Real-World Applications

2026-01-14 00:20:35

By Igbojionu Chukwudi for AIGE

HOW THIS BOOK WORKS

This book is designed as a hybrid learning guide.

Instead of teaching Python by days, it is structured around:

  • Real-world problems
  • Skills required to solve them
  • Practical examples and mini projects

Each chapter builds on the previous one.
You may complete:

  • One chapter per day
  • Multiple chapters per week
  • Or learn entirely at your own pace

All examples, explanations, and code are written for absolute beginners.

TABLE OF CONTENTS

  1. Chapter 1 – Python Foundations
  2. Chapter 2 – Working With User Data
  3. Chapter 3 – Decision Making With Code
  4. Chapter 4 – Automating Repetitive Tasks
  5. Chapter 5 – Managing Collections of Data
  6. Chapter 6 – Writing Reusable & Clean Code
  7. Chapter 7 – Building a Complete Python Application

CHAPTER 1 — PYTHON FOUNDATIONS

Problem This Solves

How do we communicate clear instructions to a computer?

What you'll learn in this chapter

By the end of this chapter you should be able to:

  • Understand what Python is (and why it's popular)
  • Run Python code on your computer
  • Use print() to display output
  • Write comments
  • Create variables (store information)
  • Follow basic naming rules

1) What is Python?

Python is a programming language you use to tell a computer what to do.

Python is used for:

  • Websites (Django, Flask)
  • Data & AI (Pandas, NumPy, ML)
  • Automation (scripts that save time)
  • Apps and tools (simple programs)

Python is beginner-friendly because it reads almost like English.

2) How to Run Python (3 common ways)

Option A: Python Interactive Mode (REPL)

Open your terminal/cmd and type:

python

You’ll see >>> then you can type code directly:

>>> print("Hello")
Hello

To exit:

exit()

Option B: Run a .py file

Create a file like day1.py and run:

python day1.py

Option C: VS Code / Cursor

  • Install Python extension
  • Create day1.py
  • Click Run ▶️

3) Your First Python Program (print)

What print() does

print() tells Python: show this on the screen.

print("Hello, world!")

Code explanation

  • print is a function
  • () means “call/execute the function”
  • "Hello, world!" is a string (text)
  • Strings usually sit inside quotes " " or ' '

4) Comments (Notes Python will ignore)

Comments help you remember what your code does.

Single-line comment

# This is a comment
print("This line will run")

Why comments matter

If you come back after 1 week, comments help you understand your own code.

5) Variables (Storing information)

A variable is like a container that stores a value.

Example:

name = "Jane"
age = 17

Explanation

  • name is the variable name
  • = means “store this value inside the variable”
  • "Jane" is stored in name
  • 17 is stored in age

6) Common Data Types

String (text)

business_name = "Snap Pro"

Integer (whole number)

items_in_stock = 25

Float (number with decimal)

price = 2500.50

Boolean (True/False)

is_open = True

7) Printing Variables (Combining text + variables)

Method 1: Comma separation (easy)

name = "Jane"
print("Hello", name)

Explanation: Python automatically adds a space between items.

Method 2: f-strings (modern and clean ✅)

name = "Jane"
age = 17
print(f"My name is {name} and I am {age} years old.")

Explanation of f-string

  • The f before the string enables formatting
  • {name} inserts the variable value inside the text
  • It’s the most common style in modern Python

8) Variable Naming Rules (Important!)

✅ Valid:

first_name = "Jane"
age2 = 20
total_sales = 5000

❌ Invalid:

2age = 20         # cannot start with number
my-name = "A"     # dash is not allowed
class = "test"    # reserved keyword

Quick rule

  • Start with a letter or _
  • Use _ for spaces (snake_case)
  • Don’t use Python reserved words (like class, def, if)

9) Real-Life Task: Personal Profile Script ✅

Task

Write a script that prints your:

  • Full name
  • Business name
  • Location
  • Skill
  • A short message

Full Code (Day1 Project)

# Day 1 Project: Personal Profile Script

full_name = "Jane"
business_name = "Snap Pro"
location = "Nigeria"
skill = "Django development"

print("=== MY PROFILE ===")
print("Full name:", full_name)
print("Business name:", business_name)
print("Location:", location)
print("Skill:", skill)

print("\nMessage:")
print(f"Hi, I'm {full_name}. I build digital solutions with {skill}.")

Code walkthrough

  • The first 4 lines create variables to store info
  • print("=== MY PROFILE ===") prints a title
  • \n means new line (it drops to the next line)
  • The last line uses an f-string to combine text + variables nicely

10) Practice Exercises

  1. Create variables for:
  • Your favorite food
  • Your favorite app
  • Your dream country
    1. Print them in a neat format.
    2. Change your variables and run again.

Example:

food = "Jollof rice"
app = "Instagram"
country = "Canada"

print(f"My favorite food is {food}.")
print(f"My favorite app is {app}.")
print(f"I would love to visit {country}.")

Mini Challenge (Optional ⭐)

Make your script look nicer:

  • Add separators like ----------
  • Add emojis inside print (Python supports it)
print("🔥 Welcome to my profile 🔥")
print("--------------------------")

CHAPTER 2 — WORKING WITH USER DATA

Problem This Solves

How do programs collect information from users and work with it?

What you'll learn in this chapter

By the end of this chapter, you will be able to:

  • Understand user input
  • Accept data from users
  • Convert input to numbers
  • Perform calculations
  • Build simple real-life interactive programs

1️⃣ Understanding input()

What is input()?

input() allows your program to receive information from the user while it’s running.

Example:

name = input("Enter your name: ")
print("Hello", name)

Explanation

  • Python pauses and waits for the user
  • Whatever the user types becomes a string
  • The value is stored in name

⚠️ Important rule:

input() ALWAYS returns a string

2️⃣ Strings vs Numbers (Very Important)

Example

age = input("Enter your age: ")
print(age)

Even if the user types 25, Python sees it as:

"25"  ← string, not number

Why this matters

You cannot do math with strings.

❌ This will cause an error:

age = input("Enter age: ")
next_year = age + 1

3️⃣ Type Conversion (Casting)

Convert string to number

age = int(input("Enter age: "))
print(age + 1)

Common conversions

Function Converts to
int() Whole numbers
float() Decimal numbers
str() Text

4️⃣ Practical Example: Simple Calculator

num1 = int(input("Enter first number: "))
num2 = int(input("Enter second number: "))

total = num1 + num2
print("Total:", total)

Code explanation

  • User enters two numbers
  • int() converts them
  • Python adds them
  • Result is printed

5️⃣ Basic Math Operators

Operator Meaning
+ Addition
- Subtraction
* Multiplication
/ Division
// Whole division
% Remainder
** Power

Example

price = 2500
qty = 3

print(price * qty)  # 7500

6️⃣ Real-Life Example: Product Total Price

product = input("Product name: ")
price = float(input("Price per item: "))
quantity = int(input("Quantity: "))

total = price * quantity

print(f"\nProduct: {product}")
print(f"Total cost: ₦{total}")

Explanation

  • float() allows decimal prices
  • int() ensures quantity is a whole number
  • f-string formats output nicely

7️⃣ Mini Project: POS (Point of Sale) Calculator ✅

Task

Build a small POS system that:

  • Accepts product name
  • Accepts price
  • Accepts quantity
  • Calculates total amount
  • Displays receipt-like output

Full Code (Day 2 Project)

print("=== SIMPLE POS SYSTEM ===")

product_name = input("Enter product name: ")
price = float(input("Enter product price: "))
quantity = int(input("Enter quantity: "))

total = price * quantity

print("\n--- RECEIPT ---")
print(f"Product: {product_name}")
print(f"Price: ₦{price}")
print(f"Quantity: {quantity}")
print(f"Total Amount: ₦{total}")

8️⃣ Code Walkthrough (Line by Line)

print("=== SIMPLE POS SYSTEM ===")

Prints a title.

product_name = input("Enter product name: ")

Receives product name as text.

price = float(input("Enter product price: "))

Converts user input to decimal number.

quantity = int(input("Enter quantity: "))

Converts user input to whole number.

total = price * quantity

Calculates total cost.

print(f"Total Amount: ₦{total}")

Displays final result using f-string.

9️⃣ Common Beginner Mistakes ⚠️

❌ Forgetting conversion:

price = input("Price: ")
print(price * 2)   # wrong

✅ Correct:

price = float(input("Price: "))
print(price * 2)

🔁 Practice Exercises (Do All)

1️⃣ Ask user for:

  • Name
  • Year of birth

2️⃣ Calculate:

  • Current age
year = int(input("Enter year of birth: "))
age = 2026 - year
print(f"You are {age} years old")

3️⃣ Modify POS to include:

  • Customer name
  • Discount (optional)

⭐ Mini Challenge

Add a 10% discount automatically if total is above ₦20,000.

if total > 20000:
    discount = total * 0.10
    total -= discount

(Don’t worry if if looks new — you’ll master it on Day 3.)

✅ Chapter 2 Outcome

✔ You can collect real data
✔ You understand data types
✔ You can perform calculations
✔ You've built an interactive program

CHAPTER 3 — DECISION MAKING WITH CODE

Problem This Solves

How does software decide approval, rejection, or status?

🎯 What you'll learn in this chapter

By the end of this chapter, you will be able to:

  • Make your program think and decide
  • Use if, elif, and else
  • Compare values correctly
  • Build logic for real-life situations (approval, rejection, status checks)

1️⃣ What Are Conditions?

Conditions allow your program to make decisions based on data.

Real-life examples:

  • If balance is enough → allow payment
  • If age ≥ 18 → allow registration
  • If stock is 0 → show “Out of stock”

In Python, we do this using:

if
elif
else

2️⃣ The if Statement (Basic Decision)

Example

age = 20

if age >= 18:
    print("You are allowed")

Explanation

  • Python checks the condition: age >= 18
  • If it is True, the code inside runs
  • If it is False, Python skips it

⚠️ Indentation matters

  • Everything inside if must be indented (usually 4 spaces)

3️⃣ if and else (Two Possible Outcomes)

Example

balance = 3000

if balance >= 5000:
    print("Transaction approved")
else:
    print("Insufficient funds")

Explanation

  • Python checks the condition
  • If True → runs if block
  • If False → runs else block

4️⃣ Comparison Operators (Very Important)

Operator Meaning
== Equal to
!= Not equal
> Greater than
< Less than
>= Greater or equal
<= Less or equal

Example

score = 75

if score >= 50:
    print("Passed")
else:
    print("Failed")

5️⃣ Using elif (Multiple Conditions)

Example

score = 85

if score >= 80:
    print("Grade A")
elif score >= 60:
    print("Grade B")
elif score >= 50:
    print("Grade C")
else:
    print("Failed")

Explanation

  • Python checks from top to bottom
  • The first true condition runs
  • Others are ignored

6️⃣ Conditions with User Input

Example

age = int(input("Enter your age: "))

if age >= 18:
    print("You are eligible")
else:
    print("You are not eligible")

Explanation

  • User input is converted to int
  • Python compares the value
  • Decision is made automatically

7️⃣ Real-Life Example: Business Discount Logic

amount = float(input("Enter total amount: "))

if amount >= 20000:
    discount = amount * 0.10
    print(f"Discount applied: ₦{discount}")
else:
    print("No discount applied")

Explanation

  • If customer spends enough → reward them
  • Else → normal pricing

8️⃣ Mini Project: Loan Eligibility Checker ✅

Task

Build a program that:

  • Asks for monthly income
  • Approves loan if income ≥ ₦100,000
  • Rejects otherwise
  • Shows clear message

Full Code (Day 3 Project)

print("=== LOAN ELIGIBILITY CHECKER ===")

income = float(input("Enter your monthly income: ₦"))

if income >= 100000:
    print("✅ Loan Approved")
    print("You qualify for the loan.")
else:
    print("❌ Loan Rejected")
    print("Income below required threshold.")

9️⃣ Code Walkthrough (Line by Line)

print("=== LOAN ELIGIBILITY CHECKER ===")

Displays program title.

income = float(input("Enter your monthly income: ₦"))
  • Receives income
  • Converts to number
if income >= 100000:
  • Condition check
print("✅ Loan Approved")
  • Runs only if condition is True
else:
  • Runs when condition is False

🔥 Common Beginner Errors & Fixes

❌ Using = instead of ==

if income = 100000:   # wrong

✅ Correct

if income == 100000:

❌ Forgetting indentation

if income > 50000:
print("Approved")  # error

✅ Correct

if income > 50000:
    print("Approved")

🔁 Practice Exercises (Do All)

Exercise 1: Voting Eligibility

age = int(input("Enter your age: "))

if age >= 18:
    print("You can vote")
else:
    print("You cannot vote")

Exercise 2: Stock Checker

stock = int(input("Enter available stock: "))

if stock > 0:
    print("Item available")
else:
    print("Out of stock")

Exercise 3: Grading System

score = int(input("Enter your score: "))

if score >= 70:
    print("Excellent")
elif score >= 50:
    print("Good")
else:
    print("Needs improvement")

⭐ Mini Challenge (Optional)

Build a Payment Status Checker

  • Ask for balance
  • Ask for price
  • If balance ≥ price → Payment successful
  • Else → Insufficient balance
balance = float(input("Balance: "))
price = float(input("Price: "))

if balance >= price:
    print("Payment successful")
else:
    print("Insufficient balance")

✅ Chapter 3 Outcome

✔ You can make decisions in code
✔ You understand conditions & comparisons
✔ You can build approval/rejection logic
✔ You're ready for loops

CHAPTER 4 — AUTOMATING REPETITIVE TASKS

Problem This Solves

How do we avoid doing the same work repeatedly?

🎯 What you'll learn in this chapter

By the end of this chapter, you will be able to:

  • Understand what loops are
  • Use for loops and while loops
  • Repeat tasks automatically
  • Build real-life programs that run multiple times
  • Control when a loop stops

1️⃣ What Is a Loop?

A loop allows Python to repeat an action multiple times.

Real-life examples

  • Recording daily sales for 7 days
  • Printing a receipt for 10 customers
  • Counting stock items
  • Repeating until user says “stop”

Instead of writing code again and again, we use loops.

2️⃣ The for Loop (When You Know How Many Times)

Basic Example

for i in range(5):
    print("Hello")

Explanation

  • range(5) means 0 to 4 (5 times)
  • i is a counter (0, 1, 2, 3, 4)
  • Code inside runs 5 times

3️⃣ Understanding range()

Code Meaning
range(5) 0 → 4
range(1, 6) 1 → 5
range(1, 10, 2) 1, 3, 5, 7, 9

Example

for day in range(1, 6):
    print("Day", day)

4️⃣ Looping Through Data (Real-Life Friendly)

products = ["Rice", "Beans", "Oil"]

for item in products:
    print(item)

Explanation

  • Python takes one item at a time from the list
  • No need to count manually

5️⃣ Real-Life Example: Daily Sales Tracker

for day in range(1, 6):
    sales = float(input(f"Enter sales for Day {day}: ₦"))
    print(f"Sales recorded: ₦{sales}")

What’s happening?

  • Loop runs 5 times
  • Each time, user enters sales
  • Python labels each day automatically

6️⃣ The while Loop (Repeat Until Condition Is False)

Basic Example

count = 1

while count <= 5:
    print("Count:", count)
    count += 1

Explanation

  • Loop runs as long as condition is True
  • count += 1 prevents infinite loop

7️⃣ Infinite Loop (⚠️ Be Careful)

while True:
    print("This will run forever")

To stop it manually:

  • Press Ctrl + C

8️⃣ Using break (Exit a Loop)

while True:
    name = input("Enter name (or 'exit'): ")

    if name == "exit":
        break

    print("Hello", name)

Explanation

  • Loop keeps running
  • Stops only when user types exit

9️⃣ Using continue (Skip One Round)

for num in range(1, 6):
    if num == 3:
        continue
    print(num)

Output

1
2
4
5

🔥 Mini Project: Daily Sales Recorder ✅

Task

Build a program that:

  • Records sales for 7 days
  • Shows total sales at the end

Full Code (Day 4 Project)

print("=== DAILY SALES RECORDER ===")

total_sales = 0

for day in range(1, 8):
    sales = float(input(f"Enter sales for Day {day}: ₦"))
    total_sales += sales

print("\nWeekly sales summary")
print(f"Total sales for the week: ₦{total_sales}")

🔍 Code Walkthrough (Line by Line)

total_sales = 0

Starts total at zero.

for day in range(1, 8):

Loop runs 7 times.

total_sales += sales

Adds each day’s sales to total.

⚠️ Common Beginner Mistakes

❌ Forgetting to update counter in while

count = 1
while count <= 5:
    print(count)

✅ Fix

count += 1

❌ Wrong indentation

for i in range(3):
print(i)

✅ Fix

for i in range(3):
    print(i)

🔁 Practice Exercises (Must Do)

Exercise 1: Print Numbers 1–10

for i in range(1, 11):
    print(i)

Exercise 2: Repeat Until User Stops

while True:
    msg = input("Type something (or 'stop'): ")

    if msg == "stop":
        break

    print(msg)

Exercise 3: Sum of Numbers

total = 0

for i in range(5):
    num = int(input("Enter number: "))
    total += num

print("Total:", total)

⭐ Mini Challenge (Optional)

Modify the sales recorder:

  • Stop early if user types 0
  • Count how many days were entered

(Hint: use break and a counter)

✅ Chapter 4 Outcome

✔ You can automate repetitive tasks
✔ You understand for and while loops
✔ You can control program flow
✔ You're ready for Lists & Data Collections

CHAPTER 5 — MANAGING COLLECTIONS OF DATA

Problem This Solves

How do applications store and manage multiple records?

🎯 What you'll learn in this chapter

By the end of this chapter, you will be able to:

  • Understand what lists are
  • Store multiple values in one variable
  • Add, remove, and access items
  • Loop through lists
  • Build simple real-life data management programs

1️⃣ What Is a List?

A list is a collection of items stored in one place.

Real-life examples

  • List of products in a shop
  • List of patients in a clinic
  • List of tasks for a day
  • List of customer names

Instead of creating many variables:

p1 = "Rice"
p2 = "Beans"
p3 = "Oil"

We use a list:

products = ["Rice", "Beans", "Oil"]

2️⃣ Creating a List

items = ["Bread", "Milk", "Eggs"]
print(items)

Explanation

  • Square brackets [] define a list
  • Items are separated by commas
  • Lists can contain text, numbers, or both

3️⃣ Accessing List Items (Indexing)

products = ["Rice", "Beans", "Oil"]

print(products[0])  # Rice
print(products[1])  # Beans
print(products[2])  # Oil

Important Rule ⚠️

Python starts counting from 0, not 1.

4️⃣ Changing List Items

products = ["Rice", "Beans", "Oil"]
products[1] = "Garri"

print(products)

Explanation

  • Index 1 (Beans) is replaced with Garri

5️⃣ Adding Items to a List

Using append() (most common)

products = ["Rice", "Beans"]
products.append("Oil")

print(products)

Explanation

  • Adds item to the end of the list

6️⃣ Removing Items from a List

Remove by name

products.remove("Beans")

Remove last item

products.pop()

7️⃣ List Length (len())

products = ["Rice", "Beans", "Oil"]
print(len(products))  # 3

Real-life use

  • Count number of items in stock
  • Count number of customers
  • Count number of days recorded

8️⃣ Looping Through a List (Very Important)

products = ["Rice", "Beans", "Oil"]

for item in products:
    print(item)

Explanation

  • Python picks each item one by one
  • Cleaner than using indexes manually

9️⃣ Lists with User Input (Dynamic Data)

items = []

for i in range(3):
    product = input("Enter product name: ")
    items.append(product)

print(items)

Explanation

  • Starts with an empty list
  • User adds items dynamically

🔥 Mini Project: Inventory Manager ✅

Task

Build a program that:

  • Allows user to add products
  • Stores them in a list
  • Displays all products
  • Shows total number of products

Full Code (Day 5 Project)

print("=== INVENTORY MANAGER ===")

inventory = []

while True:
    product = input("Enter product name (or 'exit'): ")

    if product == "exit":
        break

    inventory.append(product)
    print("Product added.")

print("\nInventory List:")
for item in inventory:
    print("-", item)

print(f"\nTotal products: {len(inventory)}")

🔍 Code Walkthrough

inventory = []

Creates an empty list.

while True:

Keeps program running.

inventory.append(product)

Adds each product.

len(inventory)

Counts total items.

⚠️ Common Beginner Mistakes

❌ Accessing invalid index

print(products[5])  # error if list has only 3 items

✅ Fix

if len(products) > 5:
    print(products[5])

❌ Forgetting quotes around strings

products.append(Rice)  # wrong

✅ Fix

products.append("Rice")

🔁 Practice Exercises (Must Do)

Exercise 1: Favorite Foods

foods = []

for i in range(3):
    food = input("Enter a food: ")
    foods.append(food)

for f in foods:
    print(f)

Exercise 2: Remove an Item

items = ["Pen", "Book", "Bag"]
items.remove("Book")
print(items)

Exercise 3: Count Items

names = ["Jane", "John", "Mary"]
print("Total names:", len(names))

⭐ Mini Challenge (Optional)

Modify Inventory Manager:

  • Prevent duplicate products
  • Show message if item already exists
if product in inventory:
    print("Item already exists")
else:
    inventory.append(product)

✅ Chapter 5 Outcome

✔ You can store multiple data values
✔ You can manage dynamic data
✔ You can loop through lists
✔ You're ready for Functions

CHAPTER 6 — WRITING REUSABLE & CLEAN CODE

Problem This Solves

How do professionals organize code for reuse?

🎯 What you'll learn in this chapter

By the end of this chapter, you will be able to:

  • Understand what functions are
  • Write your own functions
  • Pass data into functions
  • Get results back using return
  • Organize code like a professional developer

1️⃣ What Is a Function?

A function is a block of code that does a specific job and can be reused.

Real-life example

Instead of calculating profit again and again:

  • You create one function
  • Call it whenever you need it

Think of a function like:

“A machine that takes input → does work → gives output”

2️⃣ Why Functions Are Important

Without functions:

  • Code becomes long
  • You repeat yourself
  • Bugs are harder to fix

With functions:

  • Code is cleaner
  • Easier to read
  • Easier to reuse
  • Easier to maintain

3️⃣ Creating a Function (def)

Basic Function Example

def greet():
    print("Hello, welcome!")

Explanation

  • def means define function
  • greet is the function name
  • () means no input yet
  • Indentation defines the function body

4️⃣ Calling a Function

greet()
greet()

Output

Hello, welcome!
Hello, welcome!

Explanation

  • Function runs only when called
  • You can call it many times

5️⃣ Function with Parameters (Input)

Example

def greet_user(name):
    print(f"Hello {name}, welcome!")

Calling it

greet_user("Jane")
greet_user("Chuks")

Explanation

  • name is a parameter
  • Value passed is called an argument

6️⃣ Function with Multiple Parameters

def add_numbers(a, b):
    print(a + b)

add_numbers(5, 3)

Explanation

  • a and b receive values
  • Function performs calculation

7️⃣ Using return (Very Important)

Example

def calculate_profit(cost, selling_price):
    profit = selling_price - cost
    return profit

Calling it

result = calculate_profit(500, 800)
print("Profit:", result)

Explanation

  • return sends value back
  • Code after return does NOT run
  • Returned value can be stored in a variable

8️⃣ Difference Between print() and return

print() return
Displays value Sends value back
Cannot be reused Can be reused
For output only For logic

❌ Bad practice

def total(a, b):
    print(a + b)

✅ Better practice

def total(a, b):
    return a + b

9️⃣ Function with User Input

def calculate_total():
    price = float(input("Price: "))
    qty = int(input("Quantity: "))
    return price * qty

total = calculate_total()
print("Total:", total)

Explanation

  • Function handles logic
  • Main program handles output

🔥 Mini Project: Profit Calculator ✅

Task

Build a program that:

  • Uses a function to calculate profit
  • Accepts cost price and selling price
  • Displays profit or loss

Full Code (Day 6 Project)

print("=== PROFIT CALCULATOR ===")

def calculate_profit(cost_price, selling_price):
    return selling_price - cost_price

cost = float(input("Enter cost price: ₦"))
selling = float(input("Enter selling price: ₦"))

profit = calculate_profit(cost, selling)

if profit > 0:
    print(f"Profit made: ₦{profit}")
elif profit < 0:
    print(f"Loss incurred: ₦{abs(profit)}")
else:
    print("No profit, no loss")

🔍 Code Walkthrough

def calculate_profit(cost_price, selling_price):

Defines function with two inputs.

return selling_price - cost_price

Calculates and returns result.

profit = calculate_profit(cost, selling)

Stores returned value.

if profit > 0:

Decision based on function result.

⚠️ Common Beginner Mistakes

❌ Forgetting to call function

calculate_profit

✅ Correct

calculate_profit(500, 700)

❌ Forgetting return

def calc(a, b):
    a + b

✅ Correct

def calc(a, b):
    return a + b

🔁 Practice Exercises (Must Do)

Exercise 1: Greeting Function

def welcome(name):
    print(f"Welcome {name}")

welcome("Jane")

Exercise 2: Area Calculator

def area(length, width):
    return length * width

print(area(5, 4))

Exercise 3: Reuse Function

def square(num):
    return num * num

print(square(2))
print(square(5))

⭐ Mini Challenge (Optional)

Create a Simple POS Function

  • Function to calculate total
  • Function to apply discount
  • Combine both
def apply_discount(amount):
    if amount > 20000:
        return amount * 0.9
    return amount

✅ Chapter 6 Outcome

✔ You can write clean reusable code
✔ You understand functions & return values
✔ You can separate logic from output
✔ You're ready for Building a Complete Python Application

CHAPTER 7 — BUILDING A COMPLETE PYTHON APPLICATION

Problem This Solves

How do we combine all Python fundamentals into a real system?

🎯 Goal of this chapter

By the end of this chapter, you will:

  • Combine input, conditions, loops, lists, and functions
  • Build a real-life business-style program
  • Think like a junior Python developer
  • Be confident to move into Django, automation, or AI basics

🧠 What You'll Use From Previous Chapters

Chapter Concept
Chapter 1 Variables, print
Chapter 2 input(), numbers
Chapter 3 if / else
Chapter 4 loops
Chapter 5 lists
Chapter 6 functions

📦 Mini Project: Simple Business Sales Manager

📝 Project Description

Build a program that:

  • Records multiple product sales
  • Calculates total price per product
  • Stores all products sold
  • Calculates total sales
  • Allows user to stop when done
  • Displays a final summary

This is similar to:

  • A shop POS
  • A sales tracker
  • A basic inventory/sales system

🧱 Step 1: Plan the Program (Very Important)

We need:

  • A list to store products
  • A loop to keep asking for data
  • A function to calculate total
  • Conditions to stop the program
  • A summary at the end

🧩 Step 2: Define Helper Function

def calculate_total(price, quantity):
    return price * quantity

Explanation

  • Takes price and quantity
  • Returns the total amount
  • Reusable for every product

🔁 Step 3: Main Program Loop

Full Project Code (Day 7 Final)

print("=== SIMPLE SALES MANAGEMENT SYSTEM ===")

sales = []          # list to store sales
grand_total = 0     # total sales amount

def calculate_total(price, quantity):
    return price * quantity

while True:
    product = input("\nEnter product name (or 'exit'): ")

    if product.lower() == "exit":
        break

    price = float(input("Enter price: ₦"))
    quantity = int(input("Enter quantity: "))

    total = calculate_total(price, quantity)
    grand_total += total

    sales.append(product)

    print(f"Total for {product}: ₦{total}")

print("\n=== SALES SUMMARY ===")

for item in sales:
    print("-", item)

print(f"\nNumber of products sold: {len(sales)}")
print(f"Grand Total Sales: ₦{grand_total}")

🔍 Full Code Walkthrough (Section by Section)

1️⃣ Program Title

print("=== SIMPLE SALES MANAGEMENT SYSTEM ===")

Displays program heading.

2️⃣ Data Storage

sales = []
grand_total = 0
  • sales stores product names
  • grand_total accumulates total sales

3️⃣ Function Definition

def calculate_total(price, quantity):
    return price * quantity

Handles calculation logic cleanly.

4️⃣ Infinite Loop

while True:

Keeps program running until user stops it.

5️⃣ Exit Condition

if product.lower() == "exit":
    break
  • Allows user to exit safely
  • .lower() makes input case-insensitive

6️⃣ User Input & Calculation

price = float(input("Enter price: ₦"))
quantity = int(input("Enter quantity: "))

Gets numeric input.

total = calculate_total(price, quantity)

Uses function.

7️⃣ Data Storage

sales.append(product)
grand_total += total

Stores data and updates total.

8️⃣ Final Summary Output

print("\n=== SALES SUMMARY ===")

Prints results neatly.

⚠️ Common Errors to Watch Out For

❌ Forgetting to convert input

price = input("Price: ")  # wrong

✅ Correct

price = float(input("Price: "))

❌ Not using break

while True:
    print("Running forever")

🔁 Practice Improvements (Do These)

1️⃣ Store price + quantity together

sales.append((product, total))

2️⃣ Prevent empty product names

if product == "":
    print("Product name cannot be empty")
    continue

3️⃣ Add discount logic

if total > 20000:
    total *= 0.9

⭐ Mini Challenge (Advanced Beginner)

Upgrade the app to:

  • Store product, price, quantity, total
  • Display full receipt-style output

Example:

Rice - Qty: 2 - ₦5000
Beans - Qty: 1 - ₦3000

🎉 FINAL OUTCOME

After completing this book, you can:

✔ Write clean Python scripts
✔ Build interactive programs
✔ Use logic, loops, lists, and functions
✔ Create real-life tools for business
✔ Move confidently into Django, automation, or AI

NEXT STEPS

  • Python file handling
  • Error handling
  • Databases
  • Django web development

🎉 Congratulations

You have completed *Python Fundamentals *.

[DEVLOG] Payload CMS Booster: Day 1 - 2

2026-01-14 00:19:45

I've been using Payload CMS a lot, and I believe it's becoming a pretty hot emerging tech in the Next.js space. As someone who's used to building APIs in Express and developing admin dashboards in React from scratch, it gets pretty tedious when basic APIs and logic (like CRUD-ing a table or handling login screens) get repeated over and over.

Sure, I've used Prisma, Drizzle, tRPC, admin dashboard templates, and the good ol' CTRL+C CTRL+V, but still having to separately handle the backend and frontend for a feature felt like it could be easier. So you should see my face when I first tried out Payload CMS with its config-based approach of building the frontend, backend, and even the data layer of an app. It's so much quicker to declare a Payload collection, which is then generated as a frontend in the admin dashboard, backend APIs, and Drizzle schemas with TypeScript interfaces for me to use instantly. This process would've taken me hours or even days if I were to use my good ol' MERN stack, but with Payload its just instant!

I have no qualms with Payload's DX (though the documentation for advanced use-cases does leave much to be desired), but I'm starting to feel friction when working on a large Payload project with about 20+ collections. My biggest Payload project, Akasia Education, has about 30+ collections with some of them having complex relationships/join fields to multiple other collections. Having an ERD does help, but it's hard for me to actually visualize how it's implemented in Payload CMS. I would have to read each payload collection individually, see their relationship/join field, and imagine it in my head if it fits with my ERD or not. I even encountered errors that were hard to debug due to my collections having multiple relationships with other collections, where those collections have relationships with other collections, and so forth.

Usually, finding where a piece of variable/function/class is used on a project is easy in an IDE with features like go-to definitions or find all references. But finding where a Payload collection is referenced by other collections is more difficult since relationship/join fields use another collection's slug, which are just strings that cannot be used for go-to definitions/find all references in IDEs. So, being inspired by Code Canvas extension and always wanting to make a VSCode extension on my own, is it possible to build a Payload CMS collection visualizer where I can simply see how each collection relates to one another?

The idea

It's super simple: get all Payload collection in a project, analyze their relationship, and visualize them in a canvas like above. Having something like this would really help me visualize my big Payload project without having to take out a pen-and-pencil or always compare it againts my ERD.

Making sure it's possible

With nothing else to do in my free time, I just started building this extension idea. The main idea is that my extension can be activated on a Payload CMS project, and it will traverse the collections to build a visualizer to show how each collection relates to the others. Basically, I want to make something like Code Canvas, but for Payload collections.

Obivously the first thing I need to ensure is if it's even possible. Well, if Code Canvas is possible, then of course this idea should also be. Specifically, I need to make sure how I would implement this and what stack I need.

Reading VSCode extension development docs, it is possible by making a webview-based extension. Frankly, there's not much structured resource on how to build a webview VSCode extension, but after some digging, I found 2 good repositories to serve as a boilerplate: Microsoft's official example and QiyuanChen02 repo.

Ultimately, I chose QiyuanChen02's repo since its newer, but these 2 repo provides a boilerplate for VSCode webview extension using React + Vite and hot-reload. The gist of the development flow is I just build a normal React app whose built code will be rendered as a webview in VSCode, and I can pass messages between VSCode and my webview using message passing. Next is how to render the visualizer. I chose React Flow since it's super popular for this.

Now that the basic prerequisite is possible for me to build the extension, it's time to actually build the extension and not abandon it as another side project.

Actually building the extension

I forked QiyuanChen02's repo to quickly bootstrap my extension project here: https://github.com/ElysMaldov/payload-cms-booster. I called it Payload CMS Booster because I have a few other ideas for this extension to boost working with Payload in VSCode. It just works by running the debug mode (F5), it will open up another VSCode window with my custom command to show the React app in a webview. I added a new launch JSON that starts the debug VSCode window in a clean slate by disabling all other extensions except the one I'm developing and also opening an existing Payload project to test it later; I placed this new debug config in the launch.json.

For the folder structure, it's quite straightforward. I have a "root" project with the main entrypoint being the src/extension.ts. This is where I register my webview to the extension. The webview takes in the dist folder of the React webview subproject in the webview folder. I have 2 projects, each having its own dependencies. For now, this works, but I could potentially turn it into a monorepo like how Kilo Code does it.

To run the project, I need to run the dev server in the webview project and start a debugger to open up another VSCode window. In that new window, I can access the command to bring up the webview (currently the command is "Payload CMS Booster: Visualize" specified in the root package.json or VSCode calls it extension manifest). Then I can just work on the webview project like a regular React + Vite project with HMR working.

The latest progress I made with this repo is setting up an example React Flow component. I haven't really thought much about how to architect this project but I'll try applying parts of MVVM since I've been working on a lot of Flutter these days; mainly I need to separate between the data and UI layer, and use domain models a lot so I don't have to worry much about how to pass my actual payload collection data from VSCode since I can just assume that's a DTO I need to map to my domain model.

So my next step is how can I traverse all of the Payload collection in a project, analyze their relationships with each other, and structure it for my webview and finally render it in React Flow? The idea I have so far is to read the payload.config.ts, iterate through each collection to collect their slug, and build their relationship based on the existence of either a relationship/join field in that collection. This seems okay so far since a relationship/join field refers to another collection using their unique slug, so I could use this as an id for each collection. I thought about using the generated payload-types.ts file, but it seems wayyyy more complicated than a regular Payload collection file.

I'll update again soon 😸.

Building SS7 Store, A Modern E-Commerce Platform with Next.js

2026-01-14 00:13:15

I recently built SS7 Store, an e-commerce platform designed to test and validate complete shopping and payment workflows.

Tech Stack
Next.js
TypeScript
Supabase
Stripe (test payment integration)

Implemented Features
Product listing
Add to cart functionality
Buy now flow with full order tracking
Bank payment method

Note: This project is created for survey and functionality testing purposes only and is not intended for production use.

This build helped me explore real-world e-commerce logic, payment flows, and backend integration using Supabase.

Feedback and suggestions are welcome 🙌