Skip to content
Back to skills

003 Name Skill 0195b76b

ASecurity

Guidance for implementing proper asyncio task cancellation with signal handling in Python. This skill applies when implementing concurrent task runners that need graceful shutdown, handling KeyboardInterrupt/SIGINT in asyncio contexts, or managing task cleanup when using semaphores for concurrency limiting. Use when tasks involve asyncio.gather, CancelledError handling, or cleanup of tasks that haven't started execution.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
testingpythontestingdebuggingapi

Works with

  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 003-name-skill_0195b76b --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 003 Name Skill 0195b76b?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 003 Name Skill 0195b76b
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-003-name-skill-0195b76b/badge)](https://www.skillsdirectory.com/skills/tools-only-003-name-skill-0195b76b)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: cancel-async-tasks
description: Guidance for implementing proper asyncio task cancellation with signal handling in Python. This skill applies when implementing concurrent task runners that need graceful shutdown, handling KeyboardInterrupt/SIGINT in asyncio contexts, or managing task cleanup when using semaphores for concurrency limiting. Use when tasks involve asyncio.gather, CancelledError handling, or cleanup of tasks that haven't started execution.
---

# Cancel Async Tasks

## Overview

This skill provides guidance for implementing robust asyncio task cancellation in Python, particularly when dealing with signal handling (SIGINT/KeyboardInterrupt), semaphore-based concurrency limiting, and ensuring proper cleanup of all tasks including those waiting in queues.

## Key Concepts

### Signal Propagation in Asyncio

Understanding how signals interact with asyncio is critical:

1. **KeyboardInterrupt vs CancelledError**: When SIGINT is received during `asyncio.run()`, the behavior differs from catching exceptions inside async code. The event loop typically converts the interrupt to `CancelledError` that propagates through tasks.

2. **Signal handler context**: Signal handlers run in the main thread, but asyncio tasks may be in various states (running, waiting on semaphore, waiting on I/O).

3. **Event loop state**: The event loop's handling of SIGINT depends on whether it's running `asyncio.run()` vs manual loop management.

### Task Lifecycle States

When cancellation occurs, tasks can be in different states:

1. **Running tasks**: Currently executing code
2. **Awaiting tasks**: Blocked on I/O or other coroutines
3. **Semaphore-waiting tasks**: Waiting to acquire a semaphore for concurrency limiting
4. **Not-yet-started tasks**: Created but not yet scheduled

Each state requires different handling for proper cleanup.

## Potential Approaches

### Approach 1: Task Group with Exception Handling

Use `asyncio.TaskGroup` (Python 3.11+) for automatic cancellation propagation:

- TaskGroup automatically cancels remaining tasks when one fails
- Provides structured concurrency guarantees
- Consider whether this matches the cleanup requirements

### Approach 2: Manual Task Tracking with Shield

Track all task objects explicitly and handle cancellation:

- Maintain a list of all created task objects
- Use `asyncio.shield()` for cleanup operations that must complete
- Implement explicit cancellation loop for all tracked tasks

### Approach 3: Signal Handler Registration

Register explicit signal handlers for SIGINT/SIGTERM:

- Use `loop.add_signal_handler()` to register custom handlers
- Set a cancellation flag or event that tasks check
- Coordinate shutdown through the event loop

### Approach 4: Context Manager Pattern

Wrap task execution in a context manager that handles cleanup:

- `__aenter__` sets up tasks and tracking
- `__aexit__` ensures all tasks are cancelled and awaited
- Handles exceptions uniformly

## Verification Strategies

### Testing with Real Signals

**Critical**: Test with actual signals, not timeouts:

```python
# Correct approach: Use subprocess with actual SIGINT
import subprocess
import signal
import time

proc = subprocess.Popen(['python', 'script.py'])
time.sleep(1)  # Let tasks start
proc.send_signal(signal.SIGINT)
stdout, stderr = proc.communicate(timeout=5)
# Verify cleanup messages in output
```

**Incorrect approach** (gives false confidence):
- Using `asyncio.wait_for()` with timeout does not replicate SIGINT behavior
- Using `asyncio.CancelledError` directly differs from signal-triggered cancellation

### Verification Checklist

1. **Running task cleanup**: Verify tasks actively executing receive cancellation
2. **Waiting task cleanup**: Verify tasks blocked on I/O are cancelled
3. **Semaphore queue cleanup**: Verify tasks waiting on semaphore acquisition are cancelled
4. **Cleanup code execution**: Verify finally blocks and cleanup handlers run
5. **No resource leaks**: Verify file handles, connections, etc. are closed
6. **Exit code verification**: Verify process exits with expected code after interrupt

### Test Scenarios to Cover

- Interrupt when all slots are filled (max_concurrent tasks running)
- Interrupt when tasks are queued waiting for semaphore
- Interrupt during cleanup phase itself
- Rapid repeated interrupts
- Interrupt before any task starts

## Common Pitfalls

### Pitfall 1: Catching KeyboardInterrupt Inside Async Functions

**Problem**: `KeyboardInterrupt` doesn't propagate normally through asyncio - it's typically converted to `CancelledError` by the event loop.

**Symptom**: Exception handlers for `KeyboardInterrupt` inside async functions never trigger during actual Ctrl+C.

**Solution**: Handle `CancelledError` instead, or register explicit signal handlers at the event loop level.

### Pitfall 2: asyncio.gather Doesn't Cancel Queued Tasks

**Problem**: When using `asyncio.gather` with more tasks than can run concurrently (via semaphore), cancelling gather doesn't automatically cancel tasks waiting to acquire the semaphore.

**Symptom**: Tasks that haven't started don't have their cleanup code run.

**Solution**: Explicitly track all task objects and cancel them individually, not just rely on gather's cancellation.

### Pitfall 3: Testing with Timeouts Instead of Signals

**Problem**: Using `asyncio.wait_for()` timeout to simulate interruption doesn't replicate actual signal handling behavior.

**Symptom**: Tests pass but actual Ctrl+C behavior differs.

**Solution**: Use `subprocess` with `signal.SIGINT` to test actual signal handling behavior.

### Pitfall 4: Cleanup During Cancellation

**Problem**: Cleanup code itself may be cancelled if not protected.

**Symptom**: Partial cleanup, resources not released.

**Solution**: Use `asyncio.shield()` for critical cleanup operations, or handle `CancelledError` and re-raise after cleanup.

### Pitfall 5: Duplicate Exception Handling Code

**Problem**: Identical cleanup code in multiple exception handlers (`CancelledError`, `KeyboardInterrupt`, etc.).

**Symptom**: Code duplication, maintenance burden.

**Solution**: Use a single handler with `except (asyncio.CancelledError, KeyboardInterrupt)` or abstract cleanup into a helper function.

### Pitfall 6: Not Awaiting Cancelled Tasks

**Problem**: Cancelling a task and not awaiting it leaves the task in a partially-cleaned-up state.

**Symptom**: Resource leaks, warnings about pending tasks.

**Solution**: Always `await asyncio.gather(*cancelled_tasks, return_exceptions=True)` after cancelling.

## Decision Framework

When implementing async task cancellation, consider:

1. **Python version**: TaskGroup (3.11+) vs manual management
2. **Concurrency model**: Fixed pool, semaphore-limited, or unlimited
3. **Cleanup requirements**: What must happen before exit?
4. **Signal handling needs**: Just SIGINT, or also SIGTERM, SIGHUP?
5. **Testing environment**: Can tests send real signals?

## Debugging Tips

- Add logging at task entry, exit, and cancellation points
- Log the task state when cancellation is received
- Use `asyncio.current_task()` to identify which task is executing
- Check `task.cancelled()` vs `task.done()` states
- Enable asyncio debug mode: `asyncio.run(main(), debug=True)`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.