This proposal aims to enhance theoperator module by adding adefault keyword argument to theattrgetter,itemgetter andgetitem functions. This addition would allow these functions to return aspecified default value when the targeted attribute or item is missing,thereby preventing exceptions and simplifying code that handles optionalattributes or items.
Currently,attrgetter anditemgetter raise exceptions if thespecified attribute or item is absent. This limitation requiresdevelopers to implement additional error handling, leading to morecomplex and less readable code.
Introducing adefault parameter would streamline operations involvingoptional attributes or items, reducing boilerplate code and enhancingcode clarity.
A similar situation occurs withgetitem, with the added nuance thatallowing the specification of a default value in this case would resolvea longstanding asymmetry with thegetattr() built-in function.
The primary design decision is to introduce a singledefault parameterapplicable to all specified attributes or items.
This approach maintains simplicity and avoids the complexity of assigningindividual default values to multiple attributes or items. While somediscussions considered allowing multiple defaults, the increasedcomplexity and potential for confusion led to favoring a single defaultvalue for all cases (more about this below inRejected Ideas).
Proposed behaviors:
f=attrgetter("name",default=XYZ) followed byf(obj) would returnobj.name if the attribute exists, elseXYZ.f=itemgetter(2,default=XYZ) followed byf(obj) would returnobj[2] if that is valid, elseXYZ.getitem(obj,k,XYZ) orgetitem(obj,k,default=XYZ) would returnobj[k] if that is valid,elseXYZ.In the first two cases, the enhancement applies to single and multipleattribute/item retrievals, with the default value returned for anymissing attribute or item.
No functionality change is incorporated in any case if the extradefault (keyword) argument is not used.
The current behavior is unchanged:
>>>classC:...classD:...classX:...pass...classE:...pass...>>>attrgetter("D")(C)<class '__main__.C.D'>>>>attrgetter("badname")(C)Traceback (most recent call last): File"<stdin>", line1, in<module>AttributeError:type object 'C' has no attribute 'badname'>>>attrgetter("D","E")(C)(<class '__main__.C.D'>, <class '__main__.C.E'>)>>>attrgetter("D","badname")(C)Traceback (most recent call last): File"<stdin>", line1, in<module>AttributeError:type object 'C' has no attribute 'badname'>>>attrgetter("D.X")(C)<class '__main__.C.D.X'>>>>attrgetter("D.badname")(C)Traceback (most recent call last): File"<stdin>", line1, in<module>AttributeError:type object 'D' has no attribute 'badname'
With this PEP, using the proposeddefault keyword:
>>>attrgetter("D",default="noclass")(C)<class '__main__.C.D'>>>>attrgetter("badname",default="noclass")(C)'noclass'>>>attrgetter("D","E",default="noclass")(C)(<class '__main__.C.D'>, <class '__main__.C.E'>)>>>attrgetter("D","badname",default="noclass")(C)(<class '__main__.C.D'>, 'noclass')>>>attrgetter("D.X",default="noclass")(C)<class '__main__.C.D.X'>>>>attrgetter("D.badname",default="noclass")(C)'noclass'
The current behavior is unchanged:
>>>obj=["foo","bar","baz"]>>>itemgetter(1)(obj)'bar'>>>itemgetter(5)(obj)Traceback (most recent call last): File"<stdin>", line1, in<module>IndexError:list index out of range>>>itemgetter(1,0)(obj)('bar', 'foo')>>>itemgetter(1,5)(obj)Traceback (most recent call last): File"<stdin>", line1, in<module>IndexError:list index out of range
With this PEP, using the proposeddefault keyword:
>>>itemgetter(1,default="XYZ")(obj)'bar'>>>itemgetter(5,default="XYZ")(obj)'XYZ'>>>itemgetter(1,0,default="XYZ")(obj)('bar', 'foo')>>>itemgetter(1,5,default="XYZ")(obj)('bar', 'XYZ')
The current behavior is unchanged:
>>>obj=["foo","bar","baz"]>>>getitem(obj,1)'bar'>>>getitem(obj,5)Traceback (most recent call last): File"<stdin>", line1, in<module>IndexError:list index out of range
With this PEP, using the proposed extra default, positionally or witha keyword:
>>>getitem(obj,1,"XYZ")'bar'>>>getitem(obj,5,"XYZ")'XYZ'>>>getitem(obj,1,default="XYZ")'bar'>>>getitem(obj,5,default="XYZ")'XYZ'
The implementation ofattrgetter is quite direct: it implies usinggetattr and catching a possibleAttributeError. Soattrgetter("name",default=XYZ)(obj) would be like:
try:value=getattr(obj,"name")exceptAttributeError:value=XYZ
Note we cannot rely on usinggetattr with a default value, as it wouldbe impossible to distinguish what it returned on each step when anattribute chain is specified (e.g.attrgetter("foo.bar.baz",default=XYZ)).
The implementation foritemgetter andgetitem is not thateasy. The more straightforward way is also simple to define andunderstand: attempting__getitem__ and catching a possibleexception (see below). This way,itemgetter(123,default=XYZ)(obj)orgetitem(obj,123,default=XYZ) would be equivalent to:
try:value=obj[123]except(IndexError,KeyError):value=XYZ
However, for performance reasons the implementation may look morelike the following, which has the same exact behavior:
iftype(obj)==dict:value=obj.get(123,XYZ)else:try:value=obj[123]except(IndexError,KeyError):value=XYZ
Note how the verification is about the exact type and not usingisinstance; this is to ensure the exact behavior, which would beimpossible if the object is a user defined one that inheritsdictbut overwritesget (similar reason to not check if the object hasaget method).
This way, performance is better but it’s just an implementation detail,so we can keep the original explanation on how it behaves.
Regarding the exception to be captured, even if__getitem__can raiseIndexError,KeyError, orTypeError (see itsreference), only the first two can happen if the container does notcontain the indicated key or index, and the latter is likely to signala bug in the code, so we’re not capturing it to trigger the defaultbehavior.
Providing adefault option would only work if accessing theitem/attribute would fail in the normal case. In other words, theobject accessed should not handle defaults itself.
For example, the following would be redundant/confusing becausedefaultdict will never error out when accessing the item:
>>>fromcollectionsimportdefaultdict>>>fromoperatorimportitemgetter>>>dd=defaultdict(int)>>>itemgetter("foo",default=-1)(dd)0
The same applies to any user defined object that overloads__getitem__or__getattr__ implementing its own fallbacks.
The idea of allowing multiple default values for multiple attributes oritems was considered.
Two alternatives were discussed, using an iterable that must have thesame quantity of items as parameters given toattrgetter/itemgetter, or using a dictionary with keys matchingthose names passed toattrgetter/itemgetter.
The really complex thing to solve here (making thefeature hard to explain and with confusing corner cases), is what would happenif an iterable or dictionary is theactual default desired for allitems. For example:
>>>itemgetter("a",default=(1,2))({})(1, 2)>>>itemgetter("a","b",default=(1,2))({})((1, 2), (1, 2))
If we allow “multiple default values” usingdefault, the first casein the example above would raise an exception because there are more itemsthan names in the default, and the second case would return(1,2)). This iswhy we considered the possibility of using a different name for multipledefaults (e.g.defaults, which is expressive but maybe error prone becauseit is too similar todefault).
Another proposal that would enable multiple defaults, is allowingcombinations ofattrgetter anditemgetter, e.g.:
>>>ig_a=itemgetter("a",default=1)>>>ig_b=itemgetter("b",default=2)>>>ig_combined=itemgetter(ig_a,ig_b)>>>ig_combined({"a":999})(999, 2)>>>ig_combined({})(1, 2)
However, combiningitemgetter orattrgetter is totally newbehavior and very complex to define. While not impossible, it is beyondthe scope of this PEP.
In the end, having multiple default values was deemed overly complex andpotentially confusing, and a singledefault parameter was favored forsimplicity and predictability.
Another rejected proposal was adding a flag to always return a tupleregardless of how many keys/names/indices were given.E.g.:
>>>letters=["a","b","c"]>>>itemgetter(1,return_tuple=True)(letters)('b',)>>>itemgetter(1,2,return_tuple=True)(letters)('b', 'c')
This would be of little help for multiple default values consistency,requiring further discussion, and is out of the scope of thisPEP.
There are no open issues at this time.
As the basic behavior is not modified, this newdefault can beavoided when teachingattrgetter anditemgetter for the firsttime. It can be introduced only when the functionality is needed.
The proposed changes are backward-compatible. Thedefault parameteris optional; existing code without this parameter will function asbefore. Only code that explicitly uses the newdefault parameter willexhibit the new behavior, ensuring no disruption to currentimplementations.
Introducing adefault parameter does not inherently introducesecurity vulnerabilities.
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0769.rst
Last modified:2025-05-26 08:01:11 GMT