Question: I'm wondering what the story -- whether sound design or inherited legacy -- is behind these functools.partial and inspect.signature facts (talking python 3.8 here). Set up: from functools import partial from inspect import signature def bar(a, b): return a / b  All starts well with the following, which seems compliant with curry-standards. We're fixing a to 3 positionally, a disappears from the signature and it's value is indeed bound to 3: f = partial(bar, 3) assert str(signature(f)) == '(b)' assert f(6) == 0.5 == f(b=6)  If we try to specify an alternate value for a, f won't tell us that we got an unexpected keyword, but rather that it got multiple values for argument a: f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a' f(c=2, b=6) # TypeError: bar() got an unexpected keyword argument 'c'  But now if we fix b=3 through a keyword, b is not removed from the signature, it's kind changes to keyword-only, and we can still use it (overwrite the default, as a normal default, which we couldn't do with a in the previous case): f = partial(bar, b=3) assert str(signature(f)) == '(a, *, b=3)' assert f(6) == 2.0 == f(6, b=3) assert f(6, b=1) == 6.0  Why such asymmetry? It gets even stranger, we can do this: f = partial(bar, a=3) assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?  Fine: For keyword-only arguments, there can be no confusing of what parameter a default is assigned to, but I still wonder what design-thinking or constraints are behind these choices.

Answer: Using partial with a Positional Argument f = partial(bar, 3)  By design, upon calling a function, positional arguments are assigned first. Then logically, 3 should be assigned to a with partial. It makes sense to remove it from the signature as there is no way to assign anything to it again! when you have f(a=2, b=6), you are actually doing bar(3, a=2, b=6)  when you have f(2, 2), you are actually doing bar (3, 2, 2)  We never get rid of 3 For the new partial function:  We can't give a a different value with another positional argument We can't use the keyword a to assign a different value to it as it is already "filled"   If there is a parameter with the same name as the keyword, then the argument value is assigned to that parameter slot. However, if the parameter slot is already filled, then that is an error.  I recommend reading the function calling behavior section of pep-3102 to get a better grasp of this matter. Using partial with a Keyword Argument f = partial(bar, b=3)  This is a different use case. We are applying a keyword argument to bar. You are functionally turning def bar(a, b): ...  into def f(a, *, b=3): ...  where b becomes a keyword-only argument instead of def f(a, b=3): ...  inspect.signature correctly reflects a design decision of partial. The keyword arguments passed to partial are designed to append additional positional arguments (source). Note that this behavior does not necessarily override the keyword arguments supplied with f = partial(bar, b=3), i.e., b=3 will be applied regardless of whether you supply the second positional argument or not (and there will be a TypeError if you do so). This is different from a positional argument with a default value. >>> f(1, 2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: f() takes 1 positional argument but 2 were given  where f(1, 2) is equivalent to bar(1, 2, b=3) The only way to override it is with a keyword argument >>> f(2, b=2)  An argument that can only be assigned with a keyword but positionally? This is a keyword-only argument. Thus (a, *, b=3) instead of (a, b=3). The Rationale of Non-default Argument follows Default Argument f = partial(bar, a=3) assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?   You can't do def bar(a=3, b). a and b are so called positional-or-keyword arguments. You can do def bar(*, a=3, b). a and b are keyword-only arguments.  Even though semantically, a has a default value and thus it is optional, we can't leave it unassigned because b, which is a positional-or-keyword argument needs to be assigned a value if we want to use b positionally. If we do not supply a value for a, we have to use b as a keyword argument. Checkmate! There is no way for b to be a positional-or-keyword argument as we intended. The PEP for positonal-only arguments also kind of shows the rationale behind it. This also has something to do with the aforementioned "function calling behavior". partial != Currying & Implementation Details partial by its implementation wraps the original function while storing the fixed arguments you passed to it. IT IS NOT IMPLEMENTED WITH CURRYING. It is rather partial application instead of currying in the sense of functional programming. partial is essentially applying the fixed arguments first, then the arguments you called with the wrapper: def __call__(self, /, *args, **keywords): keywords = {**self.keywords, **keywords} return self.func(*self.args, *args, **keywords)  This explains f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'. See also: Why is partial called partial instead of curry Under the Hood of inspect The outputs of inspect is another story. inspect itself is a tool that produces user-friendly outputs. For partial() in particular (and partialmethod(), similarly), it follows the wrapped function while taking the fixed parameters into account: if isinstance(obj, functools.partial): wrapped_sig = _get_signature_of(obj.func) return _signature_get_partial(wrapped_sig, obj)  Do note that it is not inspect.signature's goal to show you the actual signature of the wrapped function in the AST. def _signature_get_partial(wrapped_sig, partial, extra_args=()): """Private helper to calculate how 'wrapped_sig' signature will look like after applying a 'functools.partial' object (or alike) on it. """ ...  So we have a nice and ideal signature for f = partial(bar, 3) but get f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a' in reality. Follow-up If you want currying so badly, how do you implement it in Python, in the way which gives you the expected TypeError?