|
| 1 | +<p>This rule raises an issue when <code>@singledispatch</code> is used on a class method instead of <code>@singledispatchmethod</code>, or when |
| 2 | +<code>@singledispatchmethod</code> is used on a standalone function instead of <code>@singledispatch</code>.</p> |
| 3 | +<h2>Why is this an issue?</h2> |
| 4 | +<p>The <code>@singledispatch</code> and <code>@singledispatchmethod</code> decorators from <code>functools</code> serve different purposes and are not |
| 5 | +interchangeable.</p> |
| 6 | +<p><code>@singledispatch</code> is designed for standalone functions. It dispatches based on the type of the first argument. When applied to an |
| 7 | +instance method, the first argument is <code>self</code>, so dispatch is performed on the type of the instance instead of on the actual data argument. |
| 8 | +The registered type implementations (for example, for <code>int</code> or <code>str</code>) are therefore never selected based on the data, and the |
| 9 | +behavior depends on the receiver:</p> |
| 10 | +<ul> |
| 11 | + <li>When called on an instance of the class where the method is defined, the base implementation is invoked regardless of the data argument’s |
| 12 | + type.</li> |
| 13 | + <li>When called on an instance of a subclass, dispatch may resolve to a <strong>different</strong> registered implementation depending on the |
| 14 | + subclass’s MRO, so the same call can produce different results in different parts of a class hierarchy. This is typically worse than always falling |
| 15 | + back to the base implementation, because the bug becomes intermittent and hierarchy-dependent.</li> |
| 16 | +</ul> |
| 17 | +<p><code>@singledispatchmethod</code> is designed for instance methods and class methods. It returns a non-callable descriptor that relies on the |
| 18 | +descriptor protocol to bind to an instance or class. When applied to a standalone function, the resulting object cannot be invoked at all, and calling |
| 19 | +it raises <code>TypeError: 'singledispatchmethod' object is not callable</code>.</p> |
| 20 | +<p>This rule also flags <code>@singledispatch</code> applied to a method decorated with <code>@classmethod</code>. In that case, the dispatcher |
| 21 | +attempts to call a <code>classmethod</code> object directly and raises <code>TypeError</code> at call time.</p> |
| 22 | +<p>In all of these cases, no error or warning is raised at decoration time, making the bug silent and hard to detect by reading the code alone.</p> |
| 23 | +<h3>Exceptions</h3> |
| 24 | +<p>The rule’s behavior with <code>@staticmethod</code> depends on the decorator order:</p> |
| 25 | +<ul> |
| 26 | + <li><code>@singledispatch</code> placed <strong>above</strong> <code>@staticmethod</code> <strong>is</strong> flagged. In that order, |
| 27 | + <code>@singledispatch</code> wraps the <code>staticmethod</code> descriptor object, which prevents it from working as a static method and breaks |
| 28 | + dispatch on instances (<code>MyClass().process(value)</code> raises <code>TypeError</code>).</li> |
| 29 | + <li><code>@staticmethod</code> placed <strong>above</strong> <code>@singledispatch</code> is <strong>not</strong> flagged, because |
| 30 | + <code>@singledispatch</code> is then applied to a regular function and <code>@staticmethod</code> simply prevents Python from binding |
| 31 | + <code>self</code>, so dispatch correctly happens on the data argument.</li> |
| 32 | +</ul> |
| 33 | +<p>Note that even the non-flagged order is uncommon and easy to misuse: stacked decorators must be ordered carefully, and a future refactor that swaps |
| 34 | +the order silently reintroduces the bug. Prefer using <code>@singledispatch</code> on a module-level function, or <code>@singledispatchmethod</code> |
| 35 | +when the dispatcher needs to live on a class.</p> |
| 36 | +<h3>What is the potential impact?</h3> |
| 37 | +<p>Confusing these two decorators causes the type dispatch mechanism to behave incorrectly:</p> |
| 38 | +<ul> |
| 39 | + <li>registered type implementations will not be called for the expected argument</li> |
| 40 | + <li>with <code>@singledispatch</code> on an instance method, calls on subclasses may resolve to <strong>different</strong> registered |
| 41 | + implementations than calls on the base class, so the same call site can produce different results depending on the receiver’s type</li> |
| 42 | + <li>the code may appear to work in simple tests but fail or behave inconsistently in production with different input types or in larger class |
| 43 | + hierarchies</li> |
| 44 | + <li>debugging is difficult because no error or warning is raised at decoration time</li> |
| 45 | + <li>the application may process data incorrectly, potentially leading to data corruption or security issues if type-specific validation or |
| 46 | + sanitization is bypassed</li> |
| 47 | + <li>combining <code>@singledispatch</code> with <code>@classmethod</code> raises a <code>TypeError</code> at call time</li> |
| 48 | +</ul> |
| 49 | +<h2>How to fix it</h2> |
| 50 | +<h3>Code examples</h3> |
| 51 | +<h4>Noncompliant code example</h4> |
| 52 | +<p>Using <code>@singledispatch</code> on a class method:</p> |
| 53 | +<pre data-diff-id="1" data-diff-type="noncompliant"> |
| 54 | +from functools import singledispatch |
| 55 | + |
| 56 | +class Processor: |
| 57 | + @singledispatch # Noncompliant: should use @singledispatchmethod for methods |
| 58 | + def process(self, data): |
| 59 | + return f"Processing: {data}" |
| 60 | + |
| 61 | + @process.register(int) |
| 62 | + def _(self, data): |
| 63 | + return f"Processing int: {data}" |
| 64 | + |
| 65 | + @process.register(str) |
| 66 | + def _(self, data): |
| 67 | + return f"Processing str: {data}" |
| 68 | +</pre> |
| 69 | +<h4>Compliant solution</h4> |
| 70 | +<pre data-diff-id="1" data-diff-type="compliant"> |
| 71 | +from functools import singledispatchmethod |
| 72 | + |
| 73 | +class Processor: |
| 74 | + @singledispatchmethod |
| 75 | + def process(self, data): |
| 76 | + return f"Processing: {data}" |
| 77 | + |
| 78 | + @process.register(int) |
| 79 | + def _(self, data): |
| 80 | + return f"Processing int: {data}" |
| 81 | + |
| 82 | + @process.register(str) |
| 83 | + def _(self, data): |
| 84 | + return f"Processing str: {data}" |
| 85 | +</pre> |
| 86 | +<h4>Noncompliant code example</h4> |
| 87 | +<p>Using <code>@singledispatchmethod</code> on a standalone function:</p> |
| 88 | +<pre data-diff-id="2" data-diff-type="noncompliant"> |
| 89 | +from functools import singledispatchmethod |
| 90 | + |
| 91 | +@singledispatchmethod # Noncompliant: should use @singledispatch for standalone functions |
| 92 | +def process(data): |
| 93 | + return f"Processing: {data}" |
| 94 | + |
| 95 | +@process.register(int) |
| 96 | +def _(data): |
| 97 | + return f"Processing int: {data}" |
| 98 | + |
| 99 | +@process.register(str) |
| 100 | +def _(data): |
| 101 | + return f"Processing str: {data}" |
| 102 | +</pre> |
| 103 | +<h4>Compliant solution</h4> |
| 104 | +<pre data-diff-id="2" data-diff-type="compliant"> |
| 105 | +from functools import singledispatch |
| 106 | + |
| 107 | +@singledispatch |
| 108 | +def process(data): |
| 109 | + return f"Processing: {data}" |
| 110 | + |
| 111 | +@process.register(int) |
| 112 | +def _(data): |
| 113 | + return f"Processing int: {data}" |
| 114 | + |
| 115 | +@process.register(str) |
| 116 | +def _(data): |
| 117 | + return f"Processing str: {data}" |
| 118 | +</pre> |
| 119 | +<h3>How does this work?</h3> |
| 120 | +<p>Use <code>@singledispatch</code> for standalone, module-level functions and <code>@singledispatchmethod</code> for instance methods and class |
| 121 | +methods. The <code>@singledispatch</code> decorator dispatches on the first argument, while <code>@singledispatchmethod</code> skips <code>self</code> |
| 122 | +or <code>cls</code> and dispatches on the next argument.</p> |
| 123 | +<p>When combining <code>@singledispatchmethod</code> with <code>@classmethod</code>, <code>@singledispatchmethod</code> must remain the outermost |
| 124 | +decorator so that the registration mechanism (<code>process.register</code>) is still available. <code>@classmethod</code> is then applied to the base |
| 125 | +implementation and to each registered overload:</p> |
| 126 | +<pre> |
| 127 | +from functools import singledispatchmethod |
| 128 | + |
| 129 | +class Processor: |
| 130 | + @singledispatchmethod |
| 131 | + @classmethod |
| 132 | + def process(cls, data): |
| 133 | + return f"Processing: {data}" |
| 134 | + |
| 135 | + @process.register |
| 136 | + @classmethod |
| 137 | + def _(cls, data: int): |
| 138 | + return f"Processing int: {data}" |
| 139 | +</pre> |
| 140 | +<p>For static methods, prefer turning the function into a module-level function decorated with <code>@singledispatch</code>. If the dispatcher must |
| 141 | +live on the class, place <code>@staticmethod</code> <strong>above</strong> <code>@singledispatch</code> so that <code>@singledispatch</code> wraps a |
| 142 | +regular function rather than a <code>staticmethod</code> descriptor; the inverse order breaks dispatch on instance calls and is reported by this |
| 143 | +rule.</p> |
| 144 | +<h2>Resources</h2> |
| 145 | +<h3>Documentation</h3> |
| 146 | +<ul> |
| 147 | + <li>Python Documentation - <a |
| 148 | + href="https://docs.python.org/3/library/functools.html#functools.singledispatch"><code>functools.singledispatch</code></a></li> |
| 149 | + <li>Python Documentation - <a |
| 150 | + href="https://docs.python.org/3/library/functools.html#functools.singledispatchmethod"><code>functools.singledispatchmethod</code></a></li> |
| 151 | + <li>PEP 443 - <a href="https://www.python.org/dev/peps/pep-0443/">Single-dispatch generic functions</a></li> |
| 152 | +</ul> |
| 153 | + |
0 commit comments