What is the difference between signal.SIG_DFL and signal.SIG_IGN in signal.signal()?
answer
- Two sentinels, not functions
- One restores, the other discards
- Nothing is replayed later
- Different from blocking with a mask
- getsignal can return None
basics
~10 ssignal.SIG_DFL restores the operating system's default action for that signal, such as terminating the process. signal.SIG_IGN discards the signal entirely, so neither a handler nor the default action runs.
solid answer
~40 sBoth are sentinel constants, not callables. `signal.signal(sig, signal.SIG_DFL)` puts the signal back to the kernel's default disposition — for `signal.SIGTERM` and `signal.SIGINT` that means the process dies without running any Python code. `signal.signal(sig, signal.SIG_IGN)` tells the kernel to throw the signal away: no handler, no default action, and nothing is replayed later. `signal.signal()` returns the *previous* handler, so the idiomatic way to install a temporary handler is to save that return value and pass it back afterwards, and `signal.getsignal()` reads the current one without changing it. `signal.getsignal()` returns `signal.SIG_DFL`, `signal.SIG_IGN`, a callable, or `None` when the disposition was set outside Python. Ignoring is not the same as blocking with `signal.pthread_sigmask()`, which merely delays delivery.
code
python · 10 linesimport signal
def on_usr1(signum, frame):
print("got", signal.Signals(signum).name)
previous = signal.signal(signal.SIGUSR1, on_usr1)
print("was default:", previous is signal.SIG_DFL)
signal.raise_signal(signal.SIGUSR1)
signal.signal(signal.SIGUSR1, previous)
print("restored:", signal.getsignal(signal.SIGUSR1) is signal.SIG_DFL)go deeper
Know that both are constants you pass to signal.signal, that SIG_DFL means the operating system's normal behaviour and SIG_IGN means throw the signal away, and that signal.signal hands back the previous handler.
Explain the mechanics: dispositions are per-process and survive fork, an ignored signal is discarded rather than deferred, and blocking with a signal mask is the tool when you need the signal later.
Show the operational awareness: audit what your process inherited, restore handlers in a finally block, and never leave a library-installed disposition in place across a spawn without deciding about it deliberately.
Set the convention for services you own: which signals each process type must handle, which are deliberately ignored, and how that contract is documented so orchestrators and supervisors behave predictably.
## Two sentinels and a callable `signal.signal(signalnum, handler)` accepts exactly three kinds of handler: a Python callable taking `(signum, frame)`, or one of two module-level sentinels. * **`signal.SIG_DFL`** asks the operating system for the *default disposition* of that signal. Each signal has one: `signal.SIGTERM` and `signal.SIGINT` terminate the process, `signal.SIGCHLD` is ignored, others dump core or stop the process. Restoring `SIG_DFL` for a terminating signal means the process dies inside the kernel — no Python code runs, no `finally` block, no `atexit` callback. * **`signal.SIG_IGN`** asks the kernel to discard the signal. Nothing runs and nothing is remembered: an ignored signal is *not* queued and will not be replayed when you install a handler later. Both are opaque enum members, not functions — you never call them, you pass them. ## Ignoring is not blocking This is the distinction the question is usually asked to expose. `signal.SIG_IGN` throws the signal away. `signal.pthread_sigmask()` *blocks* it: the kernel marks it pending on the thread and delivers it as soon as the mask is lifted. Use ignoring when the event is genuinely irrelevant to you; use blocking when you need a short window during which the handler must not run, and expect the signal afterwards. ## Reading and restoring `signal.signal()` returns the handler it replaced, which makes the save-and-restore idiom natural: ```python previous = signal.signal(signal.SIGUSR1, my_handler) try: ... finally: signal.signal(signal.SIGUSR1, previous) ``` `signal.getsignal(signalnum)` reads the current setting without touching it and can return four things: * the callable you installed, * `signal.SIG_DFL` or `signal.SIG_IGN` if that is the current disposition, * `None` when the disposition was set from outside Python — typically by a C extension or by the process that started you. `None` means "CPython does not know", and it is a real trap: you cannot faithfully restore a handler you were never able to read. A related trap is inheritance. Dispositions survive `os.fork()`, and across `os.execv()` an **ignored** signal stays ignored while an installed handler resets to the default. A parent that ignored a signal before spawning a child hands that child a surprising environment, which is a classic "my child process cannot be interrupted" bug. ## Signals you cannot set `signal.SIGKILL` and `signal.SIGSTOP` cannot be caught or ignored. The operating system refuses the request and `signal.signal()` raises `OSError` (EINVAL). `signal.valid_signals()` gives the set the platform actually supports, and `signal.Signals(n).name` or `signal.strsignal(n)` turn a number into something readable in a log line. Registration is also main-thread-only: calling `signal.signal()` from a worker thread raises `ValueError` no matter which sentinel you pass. ## Two special cases worth a sentence Setting `signal.SIGCHLD` to `signal.SIG_IGN` is not merely cosmetic on Unix: it tells the kernel to reap children automatically, so later `os.waitpid()` calls fail to find them. And CPython's own default for `signal.SIGINT` is not `SIG_DFL` at startup — it installs `signal.default_int_handler`, which raises `KeyboardInterrupt`. If you set `signal.SIGINT` to `signal.SIG_DFL` you do not get `KeyboardInterrupt` back; you get abrupt process death. Restoring interruptibility means passing `signal.default_int_handler`, not the sentinel. ## How to answer it Say the two sentences first — default action versus discard — then earn the follow-up by naming the three distinctions that matter in real code: ignoring is not blocking, `getsignal()` can return `None`, and `SIG_DFL` for `SIGINT` is not the interpreter's own behaviour.
- What does signal.getsignal() return when the disposition was set by a C extension rather than by Python code?None. The three other results are a callable you installed, signal.SIG_DFL, or signal.SIG_IGN; None means CPython has no record of what is installed because the disposition came from outside the Python layer. It is a warning sign for save-and-restore code: you can read that something else owns the signal, but you cannot restore it faithfully.
- Why does setting signal.SIGINT to signal.SIG_DFL stop KeyboardInterrupt from being raised?Because KeyboardInterrupt is not the operating system default — it comes from signal.default_int_handler, which CPython installs for SIGINT at startup. SIG_DFL replaces that with the kernel's own action, which is to terminate the process immediately, skipping finally blocks and atexit callbacks. To restore normal interpreter behaviour, install signal.default_int_handler explicitly.
saying these in an interview costs you the question
- Treats SIG_IGN and SIG_DFL as interchangeable
- Says an ignored signal is delivered once a handler exists
- Confuses ignoring with blocking via signal.pthread_sigmask
- Expects to install a handler for signal.SIGKILL
- Assumes getsignal always returns a callable or a sentinel
- Thinks SIG_DFL for SIGINT still raises KeyboardInterrupt