Skip to main content

Introduction to Task Signatures

Task signatures allow you to wrap a task invocation—including its arguments and execution options—into a single object. This object can then be passed around, serialized, or used to build complex workflows (canvases).

In this tutorial, you will build a flexible task execution pipeline that uses partial application and custom execution options.

Prerequisites

To follow this tutorial, you need a Celery application instance and a simple task. We will use the standard add task found in many of this project's examples (such as examples/app/myapp.py):

from celery import Celery

app = Celery('tutorial')

@app.task
def add(x, y):
return x + y

Step 1: Creating Your First Signature

The most common way to create a Signature is using the .s() shortcut on a task instance. This shortcut handles positional and keyword arguments.

# Create a signature for add(2, 2)
sig = add.s(2, 2)

# Execute it immediately
result = sig.delay()
print(f"Result: {result.get()}")

When you call add.s(2, 2), Celery creates a celery.canvas.Signature object. Because Signature inherits from dict, you can inspect it to see the task name and arguments:

print(sig)
# Output: add(2, 2)

print(dict(sig))
# Output: {'task': 'tasks.add', 'args': (2, 2), 'kwargs': {}, ...}

Step 2: Using Partial Application

Signatures can be "partial," meaning you provide some arguments now and the rest later. This is useful for creating templates that are completed by other tasks in a chain.

# Create a partial signature with only one argument
partial_sig = add.s(10)

# Complete the signature by passing the second argument to .delay()
result = partial_sig.delay(5)
print(f"10 + 5 = {result.get()}")

When you call .delay(5) on a signature that already has args=(10,), Celery prepends the new arguments. The final call becomes add(5, 10).

Step 3: Configuring Execution Options

You often need to set specific execution options like a countdown, a specific queue, or an expiration time. The Signature.set() method allows you to chain these options.

# Configure a signature to run in 5 seconds on a specific queue
sig = add.s(2, 2).set(countdown=5, queue='low-priority')

# The options are stored in the signature's 'options' dictionary
print(sig.options['countdown']) # 5

result = sig.delay()

The .set() method returns the signature itself, allowing you to chain multiple configuration calls: add.s(2, 2).set(countdown=10).set(expires=30).delay().

Step 4: Creating Immutable Signatures

By default, signatures are mutable. If they are used as a callback in a chain, they will receive the result of the previous task as their first argument. To prevent this, use an "immutable" signature with the .si() shortcut.

# A normal signature would receive the result of a previous task
# An immutable signature ignores it
immutable_sig = add.si(10, 10)

# Even if we try to pass arguments, they are ignored if immutable=True
result = immutable_sig.delay(5)
# Still executes add(10, 10), not add(5, 10, 10)
print(f"Result: {result.get()}")

You can also make an existing signature immutable using .set(immutable=True).

Step 5: Linking Callbacks

Signatures are frequently used as callbacks. You can use the .link() method to specify a task that should run if the current signature succeeds.

# Create a signature that adds 2 + 2, then links to another task
sig = add.s(2, 2)
sig.link(add.s(10)) # The result (4) will be passed to add(10, ...)

result = sig.delay()
# This effectively runs: add(10, add(2, 2))

The .link() method appends the callback to the link list inside the signature's options dictionary.

Complete Working Example

Combining these concepts, you can create a highly configured, reusable task template:

from celery import signature

# 1. Create a template with specific options
template = add.s(10).set(
queue='results',
countdown=2,
immutable=False
)

# 2. Clone and customize the template for a specific run
# Using .clone() ensures the original template remains unchanged
run_sig = template.clone(args=(20,), countdown=0)

# 3. Execute
result = run_sig.delay()
print(f"Final Result: {result.get()}") # 20 + 10 = 30

By using Signature.clone(), you can maintain a base configuration and spawn variations of it without side effects.