Registry / serialization / psygnal

psygnal

JSON →
library0.15.1pypypi✓ verified 27d ago

Psygnal is a pure Python implementation of the observer pattern, providing a fast callback and event system modeled after Qt Signals & Slots. It offers optional signature and type checking for connected slots and supports threading, all without requiring or using Qt. The current version is 0.15.1, and the library is actively maintained with regular releases.

pip install psygnal
INSTALL
IMPORT
SIG · PSYGNAL
P
psygnal
serializationpythonv0.15.1
Install
1.7s avg
Import
135ms
Disk
19MB
Pass rate
10/ 10
Env Coverage10 / 10
glibc
3.9–3.13
musl
3.9–3.13
Install & Compatibility
Where this runs
tested against v0.15.1 · pip install
no network on importno background threads
Install × environment matrix
Each cell = how many times install + import succeeded across repeated harness runs. Partial = flaky.
glibc = Debian/Ubuntu slim · musl = Alpine Linux
musl
py 3.10–3.95 runs
installs and imports cleanly · install 0.0s · import 0.146s · 20.7MB
glibc
py 3.10–3.95 runs
installs and imports cleanly · install 1.7s · import 0.124s · 21MB
19MB installed
● package 19MB
Code
Verified usage

Verified import paths — ran on the pinned version, not inferred.

Signal
✓ from psygnal import Signal
evented
✓ from psygnal import evented
Decorator for creating evented dataclasses or Pydantic models.
EventedList
✓ from psygnal.containers import EventedList
Also EventedDict, EventedSet for mutable data structures.
EventedModel
✓ from psygnal import EventedModel
A Pydantic BaseModel that emits signals on field changes.
debounced
✓ from psygnal import debounced
Decorator to debounce function calls.
throttled
✓ from psygnal import throttled
Decorator to throttle function calls.

This example demonstrates how to define a signal, connect multiple callbacks (both directly and with a decorator), emit a signal, and disconnect a callback. Note that a signal is only emitted if the value truly changes.

from psygnal import Signal class MyObject: """A simple object that emits a signal when its value changes.""" value_changed = Signal(str) def __init__(self, initial_value: str = ""): self._value = initial_value def set_value(self, new_value: str): if new_value != self._value: self._value = new_value self.value_changed.emit(self._value) # Create an instance of the object my_obj = MyObject("start") # Connect a callback function using the .connect() method def on_value_change_method(new_value: str): print(f"Callback 1 (method): The value changed to '{new_value}'!") my_obj.value_changed.connect(on_value_change_method) # Connect another callback function using the @.connect decorator @my_obj.value_changed.connect def on_value_change_decorator(new_value: str): print(f"Callback 2 (decorator): I also received: '{new_value}'!") print("Initial value set, no emission yet.") # Emit signals by changing the value print("\nSetting value to 'hello':") my_obj.set_value("hello") print("\nSetting value to 'world':") my_obj.set_value("world") print("\nSetting value to 'world' again (should not emit):") my_obj.set_value("world") # Disconnect a callback my_obj.value_changed.disconnect(on_value_change_method) print("\nDisconnected 'Callback 1'. Setting value to 'psygnal':") my_obj.set_value("psygnal")
Debug
Known issues
gotchaCross-thread signal emission requires manual queue processing. If connecting a slot to run in a different thread (`connect(thread=...)`), the `psygnal.emit_queued()` function *must* be periodically called in the target thread's event loop to process the queued callbacks. Without this, callbacks will not be invoked across threads.
fix
Ensure `psygnal.emit_queued()` is called regularly in the target thread, often integrated with an event loop (e.g., using `QTimer` for Qt applications).
affects: All versions
breakingWhen using asynchronous callbacks (`async def` functions), the async backend (`psygnal.set_async_backend()`) must be configured *before* connecting any async callbacks. Failure to do so will result in a `RuntimeError` or `RuntimeWarning` and the callback not being called.
fix
Call `psygnal.set_async_backend('asyncio')` (or 'anyio', 'trio') at the start of your application, and ensure the chosen backend's event loop is running and ready before connecting async slots.
affects: All versions
gotchaBy default, `psygnal` does not strictly check the number of arguments (nargs) or types of connected slots against the signal's signature. This can lead to runtime `TypeError` exceptions when the signal is emitted if the slot's signature is incompatible.
fix
Enable stricter checking by connecting with `signal.connect(slot_func, check_nargs=True, check_types=True)`. This will raise an error at connection time if signatures are incompatible.
affects: All versions
deprecatedUsers migrating from the older `PySignal` library might be confused by the `Signal` class naming. `psygnal`'s primary signal class is `psygnal.Signal`, while `PySignal` used `PySignal.ClassSignal` and `PySignal.Signal` (which is similar to `psygnal.SignalInstance`). The `PySignal` library itself is deprecated and unmaintained.
fix
Always import `Signal` from `psygnal` (`from psygnal import Signal`) and refer to `psygnal`'s documentation for its API.
affects: Users of PySignal (an external, deprecated library)
Errors
Common errors & fixes
ValueError: Cannot connect slot 'your_slot_function' with signature: (x: int): - Slot types (x: int) do not match types in signal. Accepted signature: (p0: str, /).
This error occurs when a slot function is connected to a signal with `check_types=True` (or `check_nargs=True`), and the slot's signature (number or types of arguments) does not match the signal's declared signature.
fix
Ensure the slot function's arguments match the types declared in the `Signal()` constructor. If the signal emits `Signal(str)`, the slot should accept a string argument. Set `check_types=False` on connect to disable type checking if the mismatch is intentional and handled by the slot, or adjust the slot's signature.
AttributeError: 'Signal' object has no attribute 'emit'
This error happens when you try to call `.emit()` (or `.connect()`) on the `Signal` class itself rather than on an instance of the signal, which is typically a class attribute of an object. `Signal` defines the emitter, but `SignalInstance` (the bound signal on an object) is what you connect to and emit from.
fix
You must create an instance of the class containing the signal, then call `.emit()` or `.connect()` on that instance's signal attribute.

```python
from psygnal import Signal

class MyObject:
    value_changed = Signal(str) # Defines the signal

my_obj = MyObject() # Create an instance of MyObject

def on_value_changed(new_value: str):
    print(f"Value changed to: {new_value}")

my_obj.value_changed.connect(on_value_changed) # Connect to the instance's signal
my_obj.value_changed.emit("new_value") # Emit from the instance's signal
```
EmitLoopError: Exception occurred during callback
This exception is raised by `psygnal` when a connected callback (slot) itself raises an unhandled exception during the signal emission process. `EmitLoopError` wraps the original exception, which can be found in its `__cause__` attribute.
fix
Catch and handle the exception within the callback function (slot) to prevent it from propagating up through the signal emission. Alternatively, use `contextlib.suppress(EmitLoopError)` around the `.emit()` call if you wish to ignore exceptions in callbacks.

```python
from psygnal import Signal

class MyEmitter:
    sig = Signal()

def bad_callback():
    raise ValueError("Something went wrong in the slot!")

emitter = MyEmitter()
emitter.sig.connect(bad_callback)

# To handle the error in the callback:
try:
    emitter.sig.emit()
except Exception as e:
    print(f"Caught: {e}") # This will be EmitLoopError

# Or, to suppress it (not recommended for general use):
from contextlib import suppress
with suppress(EmitLoopError):
    emitter.sig.emit()
```
ModuleNotFoundError: No module named 'psygnal'
The `psygnal` library is not installed in the Python environment where you are trying to import it.
fix
Install `psygnal` using pip or conda.

```bash
pip install psygnal
# or for conda users
conda install -c conda-forge psygnal
```
Upgrade
Version history
0.15.1latest on PyPI · released Jan 4, 2026
Audit
Dependencies

No dependency data recorded yet.

Agent activity
16 hits · last 30 days
node
14
Resources
psygnal — pip install psygnal · libregistry