{"id":3748,"library":"psygnal","title":"psygnal","description":"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.","status":"active","version":"0.15.1","language":"python","source_language":"en","source_url":"https://github.com/pyapp-kit/psygnal","tags":["signals","events","callbacks","observer-pattern","qt-style","threading","dataclasses","pydantic"],"install":[{"cmd":"pip install psygnal","lang":"bash","label":"Install with pip"}],"dependencies":[],"imports":[{"symbol":"Signal","correct":"from psygnal import Signal"},{"note":"Decorator for creating evented dataclasses or Pydantic models.","symbol":"evented","correct":"from psygnal import evented"},{"note":"Also EventedDict, EventedSet for mutable data structures.","symbol":"EventedList","correct":"from psygnal.containers import EventedList"},{"note":"A Pydantic BaseModel that emits signals on field changes.","symbol":"EventedModel","correct":"from psygnal import EventedModel"},{"note":"Decorator to debounce function calls.","symbol":"debounced","correct":"from psygnal import debounced"},{"note":"Decorator to throttle function calls.","symbol":"throttled","correct":"from psygnal import throttled"}],"quickstart":{"code":"from psygnal import Signal\n\nclass MyObject:\n    \"\"\"A simple object that emits a signal when its value changes.\"\"\"\n    value_changed = Signal(str)\n\n    def __init__(self, initial_value: str = \"\"):\n        self._value = initial_value\n\n    def set_value(self, new_value: str):\n        if new_value != self._value:\n            self._value = new_value\n            self.value_changed.emit(self._value)\n\n# Create an instance of the object\nmy_obj = MyObject(\"start\")\n\n# Connect a callback function using the .connect() method\ndef on_value_change_method(new_value: str):\n    print(f\"Callback 1 (method): The value changed to '{new_value}'!\")\n\nmy_obj.value_changed.connect(on_value_change_method)\n\n# Connect another callback function using the @.connect decorator\n@my_obj.value_changed.connect\ndef on_value_change_decorator(new_value: str):\n    print(f\"Callback 2 (decorator): I also received: '{new_value}'!\")\n\nprint(\"Initial value set, no emission yet.\")\n\n# Emit signals by changing the value\nprint(\"\\nSetting value to 'hello':\")\nmy_obj.set_value(\"hello\")\n\nprint(\"\\nSetting value to 'world':\")\nmy_obj.set_value(\"world\")\n\nprint(\"\\nSetting value to 'world' again (should not emit):\")\nmy_obj.set_value(\"world\")\n\n# Disconnect a callback\nmy_obj.value_changed.disconnect(on_value_change_method)\nprint(\"\\nDisconnected 'Callback 1'. Setting value to 'psygnal':\")\nmy_obj.set_value(\"psygnal\")","lang":"python","description":"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."},"warnings":[{"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).","message":"Cross-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.","severity":"gotcha","affected_versions":"All versions"},{"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.","message":"When 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.","severity":"breaking","affected_versions":"All versions"},{"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.","message":"By 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.","severity":"gotcha","affected_versions":"All versions"},{"fix":"Always import `Signal` from `psygnal` (`from psygnal import Signal`) and refer to `psygnal`'s documentation for its API.","message":"Users 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.","severity":"deprecated","affected_versions":"Users of PySignal (an external, deprecated library)"}],"env_vars":null,"search_vec":"'0.15.1':47 'activ':52 'callback':15,59 'check':30 'connect':32 'current':44 'dataclass':67 'event':17,58 'fast':14 'implement':7 'librari':50 'maintain':53 'model':19 'observ':10,61 'observer-pattern':60 'offer':25 'option':26 'pattern':11,62 'provid':12 'psygnal':1,2 'pure':5 'pydant':68 'python':6 'qt':21,42,64 'qt-style':63 'regular':55 'releas':56 'requir':39 'signal':22,57 'signatur':27 'slot':23,33 'style':65 'support':35 'system':18 'thread':36,66 'type':29 'use':41 'version':45 'without':38","created_at":"2026-04-11T17:43:01.920535+00:00","updated_at":"2026-04-16T18:14:03.031853+00:00","problems":[{"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.","cause":"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.","error":"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, /)."},{"fix":"You must create an instance of the class containing the signal, then call `.emit()` or `.connect()` on that instance's signal attribute.\n\n```python\nfrom psygnal import Signal\n\nclass MyObject:\n    value_changed = Signal(str) # Defines the signal\n\nmy_obj = MyObject() # Create an instance of MyObject\n\ndef on_value_changed(new_value: str):\n    print(f\"Value changed to: {new_value}\")\n\nmy_obj.value_changed.connect(on_value_changed) # Connect to the instance's signal\nmy_obj.value_changed.emit(\"new_value\") # Emit from the instance's signal\n```","cause":"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.","error":"AttributeError: 'Signal' object has no attribute 'emit'"},{"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.\n\n```python\nfrom psygnal import Signal\n\nclass MyEmitter:\n    sig = Signal()\n\ndef bad_callback():\n    raise ValueError(\"Something went wrong in the slot!\")\n\nemitter = MyEmitter()\nemitter.sig.connect(bad_callback)\n\n# To handle the error in the callback:\ntry:\n    emitter.sig.emit()\nexcept Exception as e:\n    print(f\"Caught: {e}\") # This will be EmitLoopError\n\n# Or, to suppress it (not recommended for general use):\nfrom contextlib import suppress\nwith suppress(EmitLoopError):\n    emitter.sig.emit()\n```","cause":"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.","error":"EmitLoopError: Exception occurred during callback"},{"fix":"Install `psygnal` using pip or conda.\n\n```bash\npip install psygnal\n# or for conda users\nconda install -c conda-forge psygnal\n```","cause":"The `psygnal` library is not installed in the Python environment where you are trying to import it.","error":"ModuleNotFoundError: No module named 'psygnal'"}],"ecosystem":"pypi","meta_description":null,"install_score":null,"quickstart_score":null,"quickstart_tag":null,"pypi_latest":"0.15.1","cli_name":"","cli_version":null,"type":"library","homepage":null,"github":"https://github.com/pyapp-kit/psygnal","docs":"https://psygnal.readthedocs.io","changelog":null,"pypi":"https://pypi.org/project/psygnal/","npm":null,"openapi_spec":null,"status_page":null,"smithery":null,"categories":["serialization","http-networking"],"base_url":null,"auth_type":null,"provenance":{"verified_status":"passing","verified_at":"2026-06-28","last_verified":"2026-08-29","next_check":"2026-07-28","install_tag":null}}