Repository: django/django

Issue:
# Fixed #36951 -- Removed empty exc_info from log_task_finished signal handler.

#### Trac ticket number
N/A

#### Branch description

If no exception occurred, the logger in `django.tasks.signals.log_task_finished` would print `None Type: None`

Before:

	Task id=191 path=tests.dummy.tasks.dummy_recurring_task state=RUNNING
	dummy recurring task
	Task id=191 path=tests.dummy.tasks.dummy_recurring_task state=SUCCESSFUL
	NoneType: None

After:

	Task id=193 path=tests.dummy.tasks.dummy_recurring_task state=RUNNING
	dummy recurring task
	Task id=193 path=tests.dummy.tasks.dummy_recurring_task state=SUCCESSFUL

Of course, if there is an exception, it is still printed:

	Task id=195 path=tests.dummy.tasks.dummy_recurring_task state=RUNNING
	Traceback (most recent call last):
	  File "/Users/elias/dev/steady_queue/steady_queue/models/claimed_execution.py", line 93, in perform
	    task.func(*args, **kwargs)
	    ~~~~~~~~~^^^^^^^^^^^^^^^^^
	  File "/Users/elias/dev/steady_queue/tests/dummy/tasks.py", line 53, in dummy_recurring_task
	    return 1 / 0
		   ~~^~~
	ZeroDivisionError: division by zero

#### AI Assistance Disclosure (REQUIRED)

- [ ] **No AI tools were used** in preparing this PR.
- [x] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.

AI tools used: `cursor-agent` with `Claude Opus 4.6 (thinking)`.

#### Checklist
- [x] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [x] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [ ] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.


Implementation patch:
diff --git a/django/tasks/signals.py b/django/tasks/signals.py
index 288fe08e328a..919dae022221 100644
--- a/django/tasks/signals.py
+++ b/django/tasks/signals.py
@@ -49,6 +49,8 @@ def log_task_started(sender, task_result, **kwargs):
 
 @receiver(task_finished)
 def log_task_finished(sender, task_result, **kwargs):
+    # Signal is sent inside exception handlers, so exc_info() is available.
+    exc_info = sys.exc_info()
     logger.log(
         (
             logging.ERROR
@@ -59,6 +61,5 @@ def log_task_finished(sender, task_result, **kwargs):
         task_result.id,
         task_result.task.module_path,
         task_result.status,
-        # Signal is sent inside exception handlers, so exc_info() is available.
-        exc_info=sys.exc_info(),
+        exc_info=exc_info if exc_info[0] else None,
     )


Test patch:
diff --git a/tests/tasks/test_immediate_backend.py b/tests/tasks/test_immediate_backend.py
index 356e9ab2649d..36b63faff801 100644
--- a/tests/tasks/test_immediate_backend.py
+++ b/tests/tasks/test_immediate_backend.py
@@ -228,6 +228,15 @@ def test_failed_logs(self):
         self.assertIn("state=FAILED", captured_logs.output[2])
         self.assertIn(result.id, captured_logs.output[2])
 
+    def test_successful_task_no_none_in_logs(self):
+        with self.assertLogs("django.tasks", level="DEBUG") as captured_logs:
+            result = test_tasks.noop_task.enqueue()
+
+        self.assertEqual(result.status, TaskResultStatus.SUCCESSFUL)
+
+        for log_output in captured_logs.output:
+            self.assertNotIn("None", log_output)
+
     def test_takes_context(self):
         result = test_tasks.get_task_id.enqueue()