{"id":37416025,"url":"https://github.com/jeremyephron/pyterminate","last_synced_at":"2026-01-16T06:01:40.123Z","repository":{"id":42082793,"uuid":"473040380","full_name":"jeremyephron/pyterminate","owner":"jeremyephron","description":"Exit Python programs gracefully","archived":false,"fork":false,"pushed_at":"2025-06-28T00:10:19.000Z","size":30,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-11-10T02:17:19.751Z","etag":null,"topics":["cleanup","error-handling","pypi-package","python","python3","termination","utility"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jeremyephron.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"jeremyephron"}},"created_at":"2022-03-23T04:49:13.000Z","updated_at":"2024-12-24T03:13:08.000Z","dependencies_parsed_at":"2022-09-14T05:10:18.297Z","dependency_job_id":null,"html_url":"https://github.com/jeremyephron/pyterminate","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/jeremyephron/pyterminate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyephron%2Fpyterminate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyephron%2Fpyterminate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyephron%2Fpyterminate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyephron%2Fpyterminate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeremyephron","download_url":"https://codeload.github.com/jeremyephron/pyterminate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyephron%2Fpyterminate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28477585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T03:13:13.607Z","status":"ssl_error","status_checked_at":"2026-01-16T03:11:47.863Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cleanup","error-handling","pypi-package","python","python3","termination","utility"],"created_at":"2026-01-16T06:00:49.781Z","updated_at":"2026-01-16T06:01:40.039Z","avatar_url":"https://github.com/jeremyephron.png","language":"Python","funding_links":["https://github.com/sponsors/jeremyephron"],"categories":[],"sub_categories":[],"readme":"# pyterminate\n[![CI](https://github.com/jeremyephron/pyterminate/actions/workflows/ci.yml/badge.svg)](https://github.com/jeremyephron/pyterminate/actions/workflows/ci.yml)\n[![PyPI Downloads](https://img.shields.io/pypi/dm/pyterminate.svg?label=PyPI%20downloads)](\nhttps://pypi.org/project/pyterminate/)\n\nReliably run cleanup code upon program termination.\n\n## Table of Contents\n\n- [Why does this exist?](#why-does-this-exist)\n- [What can it do?](#what-can-it-do)\n- [Quickstart](#quickstart)\n- [Tips, tricks, and other notes](#tips-tricks-and-other-notes)\n  - [Duplicate registration after forking](#duplicate-registration-after-forking)\n  - [Multiprocessing start method](#multiprocessing-start-method)\n\n## Why does this exist?\n\nThere are currently two builtin modules for handling termination behavior\nin Python: [`atexit`](https://docs.python.org/3/library/atexit.html) and\n[`signal`](https://docs.python.org/3/library/signal.html). However, using them\ndirectly leads to a lot of repeated boilerplate code, and some non-obvious\nbehaviors that can be easy to accidentally get wrong, which is why I wrote this\npackage.\n\nThe `atexit` module is currently insufficient since it fails to handle signals.\nThe `signal` module is currently insufficient since it fails to handle normal\nor exception-caused exits.\n\nTypical approaches would include frequently repeated code registering a\nfunction both with `atexit` and on desired signals. However, extra care\nsometimes needs to be taken to ensure the function doesn't run twice (or is\nidempotent), and that a previously registered signal handler gets called.\n\n## What can it do?\n\nThis packages does or allows the following behavior:\n\n- Register a function to be called on program termination\n    - Always on normal or exception-caused termination: `@pyterminate.register`\n    - Configurable for any desired signals:\u003cbr/\u003e\n      `@pyterminate.register(signals=(signal.SIGINT, signal.SIGABRT))`\n\n- Allows multiple functions to be registered\n\n- Will call previous registered signal handlers\n\n- Allows zero or non-zero exit codes on captured signals:\u003cbr/\u003e\n  `@pyterminate.register(successful_exit=True)`\n\n- Allows suppressing or throwing of `KeyboardInterrupt` on `SIGINT`:\u003cbr/\u003e\n  `@pyterminate.register(keyboard_interrupt_on_sigint=True)`\n    - You may want to throw a `KeyboardInterrupt` if there is additional\n      exception handling defined.\n\n- Allows functions to be unregistered: `pyterminate.unregister(func)`\n\n- Ignore requested signals while registered function is executing, ensuring\n  that it is not interrupted.\n  - It's important to note that `SIGKILL` and calls to `os._exit()` cannot be\n    ignored.\n\n## Quickstart\n\n```bash\npython3 -m pip install pyterminate\n```\n\n```python3\nimport signal\n\nimport pyterminate\n\n\n@pyterminate.register(\n    args=(None,),\n    kwargs={\"b\": 42},\n    signals=(signal.SIGINT, signal.SIGTERM),\n    successful_exit=True,\n    keyboard_interrupt_on_sigint=True\n)\ndef cleanup(*args, **kwargs):\n    ...\n\n# or\n\npyterminate.register(cleanup, ...)\n```\n\n## Tips, tricks, and other notes\n\n### Duplicate registration after forking\n\nSince creating a new process through forking duplicates the entire process,\nany previously registered functions will also be registered in the forked\nprocess. This is an obvious consequence of forking, but important to \nconsider if the registered functions are accessing shared resources. To \navoid this behavior, you can unregister the function at the beginning of\nthe forked process, gate based on the process' ID, or use any other \nsynchronization method that's appropriate.\n\n### Multiprocessing start method\n\nWhen starting processes with Python's\n[`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html)\nmodule, the `fork` method will fail to call registered functions on exit, since\nthe process is ended with `os._exit()` internally, which bypasses all cleanup\nand immediately kills the process.\n\nOne way of getting around this are using the `\"spawn\"` start method if that\nis acceptable for your application. Another method is to register your function\nto a user-defined signal, and wrap your process code in try-except block,\nraising the user-defined signal at the end. `pyterminate` provides this\nfunctionality in the form of the `exit_with_signal` decorator, which simply\nwraps the decorated function in a try-finally block, and raises the given\nsignal. Example usage:\n\n```python3\nimport multiprocessing as mp\nimport signal\n\nimport pyterminate\n\n\n@pyterminate.exit_with_signal(signal.SIGUSR1)\ndef run_process():\n\n    @pyterminate.register(signals=[signal.SIGUSR1, signal.SIGINT, signal.SIGTERM])\n    def cleanup():\n        ...\n\n    ...\n\n\nif __name__ == \"__main__\"\n    mp.set_start_method(\"fork\")\n\n    proc = mp.Process(target=run_process)\n    proc.start()\n\n    try:\n        proc.join(timeout=300)\n    except TimeoutError:\n        proc.terminate()\n        proc.join()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeremyephron%2Fpyterminate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeremyephron%2Fpyterminate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeremyephron%2Fpyterminate/lists"}