Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b36972d
first steps: don't show help output for dunders we've grabbed from bu…
adqm Aug 16, 2025
93ac0fa
better help for built-in help function
adqm Aug 16, 2025
0c0f04c
add __main__ and DUNDERMETHODS to help
adqm Aug 16, 2025
255b603
try to associate all dunder methods with relevant pages from datamode…
adqm Aug 16, 2025
da7f55c
show help for, e.g., __import__
adqm Aug 17, 2025
3294f92
bugfix for sequence-related dunders
adqm Aug 17, 2025
2aee131
list 'specialnames' as a special topic
adqm Aug 17, 2025
bff3522
add help for __name__
adqm Aug 18, 2025
d1959d2
change title of added reference
adqm Aug 17, 2025
1bc0227
faster check for dunder name
adqm Aug 18, 2025
4ed0e72
suggest online help as well if we can't find something
adqm Aug 18, 2025
5800535
fix bad reference (SPECIALNAMES doesn't exist)
adqm Aug 18, 2025
587740b
change help(help) output
adqm Aug 18, 2025
69265aa
clarify message when an entry is not found
adqm Aug 18, 2025
101fff3
update test cases for new message
adqm Aug 18, 2025
35b4470
revert changes to help(help); those should probably be a separate PR
adqm Aug 19, 2025
e196818
new test cases for help() about dunder methods
adqm Aug 19, 2025
d11fb77
remove extra whitespace
adqm Aug 19, 2025
2c34fba
__matmul__ shouldn't reference @ because of where help('@') currently…
adqm Aug 19, 2025
595b95c
fixes for missing dunder doctest
adqm Aug 19, 2025
0a49cfb
attempt to fix failing test case on windows
adqm Aug 19, 2025
394e401
add news blurb
adqm Aug 19, 2025
685b627
roll back changes to pydoc_data not relevant to the new topics
adqm Aug 20, 2025
376e3fb
merge
adqm Oct 12, 2026
8c16306
fixes for checking whether a string should look up special dunder help
adqm Oct 12, 2026
f6c4f8c
add sphinx reference to news entry
adqm Oct 12, 2026
8991bb2
fix typo in __set_name__ dunder
adqm Oct 12, 2026
7f92fbe
add class-customization to pydoc topics
adqm Oct 12, 2026
d419cad
what's new entry
adqm Oct 12, 2026
f3f2f4d
add symbols to html help from pydoc
adqm Oct 12, 2026
b826b60
remove duplicate customization label from Doc/using/windows.rst
adqm Oct 12, 2026
e1c7fab
Merge branch 'main' into pydoc_dunders
adqm Oct 13, 2026
58de00a
add more built-in modules to SKIP_DUNDER_HELP
adqm Oct 13, 2026
f4b6f5c
also unquote url in error page
adqm Oct 13, 2026
cac3a02
merge
adqm Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Doc/library/__main__.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _`__main__`:

:mod:`!__main__` --- Top-level code environment
===============================================

Expand Down
2 changes: 2 additions & 0 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2991,6 +2991,8 @@ automatic property creation, proxies, frameworks, and automatic resource
locking/synchronization.


.. _customize-instance-subclass-checks:

Customizing instance and subclass checks
----------------------------------------

Expand Down
4 changes: 4 additions & 0 deletions Doc/tools/extensions/pydoc_topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
"callable-types",
"calls",
"class",
"class-customization",
"comparisons",
"compound",
"context-managers",
"continue",
"conversions",
"customization",
"customize-instance-subclass-checks",
"debugger",
"del",
"dict",
Expand All @@ -70,6 +72,7 @@
"lambda",
"lazy",
"lists",
"name_equals_main",
"match",
"naming",
"nonlocal",
Expand Down Expand Up @@ -102,6 +105,7 @@
"while",
"with",
"yield",
"__main__",
Comment thread
ZeroIntensity marked this conversation as resolved.
})


Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,9 @@ pydoc
inherited from other classes are collapsed by default.
(Contributed by Serhiy Storchaka in :gh:`153906`.)

* Add help for more special names to the built-in :func:`help` function.
(Contributed by Adam Hartz in :gh:`137966`.)


re
--
Expand Down
178 changes: 163 additions & 15 deletions Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class or function within a module or module in a package. If the
import textwrap
import time
import tokenize
import urllib.parse
import warnings
from annotationlib import Format
from collections import deque
Expand Down Expand Up @@ -1712,6 +1713,15 @@ def locate(path, forceload=0):
object = getattr(object, part)
except AttributeError:
return None
if (
_is_dunder_name(path)
and not isinstance(object, (type, type(__import__)))
and path not in SKIP_DUNDER_HELP
):
# if we're looking up a special variable and we don't find a class or a
# function, it's probably not what the user wanted (if it is, they can
# look up builtins.whatever)
return None
return object

# --------------------------------------- interactive interpreter interface
Expand All @@ -1725,10 +1735,15 @@ def resolve(thing, forceload=0):
if isinstance(thing, str):
object = locate(thing, forceload)
if object is None:
if _is_dunder_name(thing):
special = "Use help('specialnames') for a list of special names for which help is available.\n"
else:
special = ""
raise ImportError('''\
No Python documentation found for %r.
Use help() to get the interactive help utility.
Use help(str) for help on the str class.''' % thing)
No help entry found for %r.
%sUse help() to get the interactive help utility.
Use help(str) for help on the str class.
Additional documentation is available online at https://docs.python.org/%s.%s/''' % (thing, special, *sys.version_info[:2]))
Comment thread
ZeroIntensity marked this conversation as resolved.
return object, thing
else:
name = getattr(thing, '__name__', None)
Expand Down Expand Up @@ -1819,10 +1834,10 @@ def _introdoc():
Python, you should definitely check out the tutorial at
https://docs.python.org/{ver}/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules. To get a list of available
modules, keywords, symbols, or topics, enter "modules", "keywords",
"symbols", or "topics".
Enter the name of any module, keyword, symbol, or topic to get help on
writing Python programs and using Python modules. To get a list of
available modules, keywords, symbols, special names, or topics, enter
"modules", "keywords", "symbols", "specialnames", or "topics".
{pyrepl_keys}
Each module also comes with a one-line summary of what it does; to list
the modules whose name or summary contain a given string such as "spam",
Expand All @@ -1832,6 +1847,102 @@ def _introdoc():
enter "q", "quit" or "exit".
''')

SKIP_DUNDER_HELP = frozenset({
'__future__',
'__hello__',
'__phello__',
})

def _is_dunder_name(x):
return isinstance(x, str) and x.isidentifier() and len(x) > 4 and x[:2] == x[-2:] == '__'

def collect_dunders(symbols):
dunders = {
'__name__': ('name_equals_main', ''),
'__main__': ('__main__', ''),
'__call__': ('callable-types', 'SPECIALMETHODS'),
}

basic_dunders = [
'__new__', '__init__', '__del__', '__repr__', '__str__', '__bytes__',
'__format__', '__hash__', '__bool__',
]
for bd in basic_dunders:
dunders[bd] = ('customization', 'SPECIALMETHODS')

attribute_dunders = [
'__getattr__', '__getattribute__', '__setattr__', '__delattr__',
'__dir__', '__get__', '__set__', '__delete__', '__objclass__',
]
for ad in attribute_dunders:
dunders[ad] = ('attribute-access', 'SPECIALMETHODS')

class_dunders = [
'__init_subclass__', '__set_name__', '__mro_entries__',
]
for cd in class_dunders:
dunders[cd] = ('class-customization', 'SPECIALMETHODS')

instance_dunders = [
'__instancecheck__', '__subclasscheck__'
]
for d in instance_dunders:
dunders[d] = ('customize-instance-subclass-checks', 'SPECIALMETHODS')

sequence_dunders = [
'__len__', '__length_hint__', '__getitem__', '__setitem__',
'__delitem__', '__missing__', '__iter__', '__reversed__',
'__contains__',
]
for sd in sequence_dunders:
dunders[sd] = ('sequence-types', 'SPECIALMETHODS')

comparison_dunders = {
'__lt__': '<',
'__le__': '<=',
'__eq__': '==',
'__ne__': '!=',
'__gt__': '>',
'__ge__': '>=',
}
for dunder, symbol in comparison_dunders.items():
dunders[dunder] = ('customization', f'{symbol} SPECIALMETHODS')
if symbol in symbols:
symbols[symbol] += f' {dunder}'

arithmetic_dunders = {
'__add__': '+',
'__sub__': '-',
'__mul__': '*',
'__truediv__': '/',
'__floordiv__': '//',
'__mod__': '%',
'__pow__': '**',
'__lshift__': '<<',
'__rshift__': '>>',
'__and__': '&',
'__or__': '|',
'__xor__': '^',
}
for dunder, symbol in arithmetic_dunders.items():
rname = "__r" + dunder[2:]
iname = "__i" + dunder[2:]
dunders[dunder] = ('numeric-types', f'{symbol} {rname} {iname} SPECIALMETHODS')
dunders[rname] = ('numeric-types', f'{symbol} {dunder} SPECIALMETHODS')
dunders[iname] = ('numeric-types', f'{symbol} {dunder} SPECIALMETHODS')
Comment thread
ZeroIntensity marked this conversation as resolved.
if symbol in symbols:
symbols[symbol] += f' {dunder}'

# __matmul__ isn't included above because help('@') doesn't talk about
# matrix multiplication, so we shouldn't list it here as a related topic.
dunders['__matmul__'] = ('numeric-types', f'__rmatmul__ __imatmul__ SPECIALMETHODS')
dunders['__rmatmul__'] = ('numeric-types', f'__matmul__ SPECIALMETHODS')
dunders['__imatmul__'] = ('numeric-types', f'__matmul__ SPECIALMETHODS')

dunders['__divmod__'] = ('numeric-types', 'divmod')

return dunders

class Helper:

# These dictionaries map a topic name to either an alias, or a tuple
Expand Down Expand Up @@ -1915,7 +2026,8 @@ class Helper:
'(': 'TUPLES FUNCTIONS CALLS',
')': 'TUPLES FUNCTIONS CALLS',
'[': 'LISTS SUBSCRIPTS SLICINGS',
']': 'LISTS SUBSCRIPTS SLICINGS'
']': 'LISTS SUBSCRIPTS SLICINGS',

}
for topic, symbols_ in _symbols_inverse.items():
for symbol in symbols_:
Expand Down Expand Up @@ -1965,8 +2077,7 @@ class Helper:
'BASICMETHODS': ('customization', 'hash repr str SPECIALMETHODS'),
'ATTRIBUTEMETHODS': ('attribute-access', 'ATTRIBUTES SPECIALMETHODS'),
'CALLABLEMETHODS': ('callable-types', 'CALLS SPECIALMETHODS'),
'SEQUENCEMETHODS': ('sequence-types', 'SEQUENCES SEQUENCEMETHODS '
'SPECIALMETHODS'),
'SEQUENCEMETHODS': ('sequence-types', 'SEQUENCES SPECIALMETHODS'),
'MAPPINGMETHODS': ('sequence-types', 'MAPPINGS SPECIALMETHODS'),
'NUMBERMETHODS': ('numeric-types', 'NUMBERS AUGMENTEDASSIGNMENT '
'SPECIALMETHODS'),
Expand Down Expand Up @@ -2011,8 +2122,15 @@ class Helper:
'TRUTHVALUE': ('truth', 'if while and or not BASICMETHODS'),
'DEBUGGING': ('debugger', 'pdb'),
'CONTEXTMANAGERS': ('context-managers', 'with'),
'DUNDERMETHODS': 'SPECIALMETHODS',
'MAINMODULE': '__main__',
}

# add dunder methods
dunders = collect_dunders(symbols)
topics |= dunders


def __init__(self, input=None, output=None):
self._input = input
self._output = output
Expand Down Expand Up @@ -2086,6 +2204,8 @@ def help(self, request, is_cli=False):
if request == 'keywords': self.listkeywords()
elif request == 'symbols': self.listsymbols()
elif request == 'topics': self.listtopics()
elif request == 'specialnames':
self.listdunders()
elif request == 'modules': self.listmodules()
elif request[:8] == 'modules ':
self.listmodules(request.split()[1])
Expand Down Expand Up @@ -2137,7 +2257,14 @@ def listtopics(self):
Here is a list of available topics. Enter any topic name to get more help.

''')
self.list(self.topics.keys(), columns=3)
self.list([k for k in self.topics if k not in self.dunders], columns=3)

def listdunders(self):
self.output.write('''
Here is a list of special names for which help is available. Enter any one to get more help.

''')
self.list(self.dunders.keys(), columns=3)

def showtopic(self, topic, more_xrefs=''):
try:
Expand Down Expand Up @@ -2190,6 +2317,9 @@ def _gettopic(self, topic, more_xrefs=''):
Sorry, topic and keyword documentation is not available because the
module "pydoc_data.topics" could not be found.
''' , '')
if topic in self.symbols:
topic, _, xrefs = self.symbols[topic].partition(' ')
more_xrefs = ' '.join([xrefs, more_xrefs]).strip()
target = self.topics.get(topic, self.keywords.get(topic))
if not target:
raise ValueError('could not find topic')
Expand Down Expand Up @@ -2563,14 +2693,14 @@ def onerror(modname):

# format page
def bltinlink(name):
return '<a href="%s.html">%s</a>' % (name, name)
return '<a href="%s.html">%s</a>' % (urllib.parse.quote(name), html.escape(name))

results = []
heading = html.heading('Search Results')
for name, desc in search_result:
results.append('<li>%s%s</li>' % (bltinlink(name), desc))
contents = heading + html.bigsection(
'key = %s' % key, 'index', '<ul>\n%s\n</ul>' % '\n'.join(results))
'key = %s' % html.escape(key), 'index', '<ul>\n%s\n</ul>' % '\n'.join(results))
return 'Search Results', contents

def html_topics():
Expand All @@ -2596,13 +2726,27 @@ def bltinlink(name):
contents = heading + html.multicolumn(names, bltinlink)
return 'Keywords', contents

def html_symbols():
"""Index of symbols available."""

def bltinlink(name):
return '<a href="topic?key=%s">%s</a>' % (urllib.parse.quote(name), html.escape(name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

urllib.parse.quote does extra things that seemingly don't work with the .html URL handler (such as converting ! to %21). I think we can switch to html.escape(name) if all we're after is the quote escaping?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually originally worried about +, since I know that + is often interpreted as a space in a URL. So I leaned toward urllib.parse.quote just to be safe since I'm not sure what else might be special in the URL bar, and because urllib.parse.quote just felt more right to me in principle, based on the context. I also expect the difference to be largely invisible to the end user, other than slightly-grosser URLs if copy/pasting.

I hadn't noticed any broken pages in my original testing, though I did just now test searching for ! and indeed it shows a bad error message (FWIW, the error is not because of the escaping but because ! doesn't have an entry).

The change I just pushed tries to remedy that by unquoting the given URL on the error page (I was already unquoting when actually doing the lookup, so, e.g., searching for != should have worked before this most recent change).

If you'd prefer, though, I can switch it to use html.escape instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would show up if you had a module with some weird Unicode characters in the name. I suppose another fix would be to call urllib.parse.unquote in get_html_page, if you prefer that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I already have the unquote in the only path in get_html_page where these links could possibly lead (behind the check for if '=' in url, this line in the diff).

I could be missing something, though; is there a case that line doesn't cover that I'm not seeing?


heading = html.heading('Symbols')
names = sorted(Helper.symbols.keys())

contents = heading + html.multicolumn(names, bltinlink)
return 'Symbols', contents

def html_topicpage(topic):
"""Topic or keyword help page."""
buf = io.StringIO()
htmlhelp = Helper(buf, buf)
contents, xrefs = htmlhelp._gettopic(topic)
if topic in htmlhelp.keywords:
title = 'KEYWORD'
elif topic in htmlhelp.symbols:
title = 'SYMBOL'
else:
title = 'TOPIC'
heading = html.heading('%s %s' % (title.capitalize(), topic))
Expand Down Expand Up @@ -2631,7 +2775,7 @@ def html_error(url, exc):
heading = html.heading('Error')
contents = '<br>'.join(html.escape(line) for line in
format_exception_only(type(exc), exc))
contents = heading + html.bigsection(url, 'error', contents)
contents = heading + html.bigsection(html.escape(urllib.parse.unquote(url)), 'error', contents)
return "Error - %s" % url, contents

def get_html_page(url):
Expand All @@ -2646,8 +2790,11 @@ def get_html_page(url):
title, content = html_topics()
elif url == "keywords":
title, content = html_keywords()
elif url == "symbols":
title, content = html_symbols()
elif '=' in url:
op, _, url = url.partition('=')
url = urllib.parse.unquote(url).strip()
if op == "search?key":
title, content = html_search(url)
elif op == "topic?key":
Expand Down Expand Up @@ -2828,7 +2975,8 @@ class BadUsage(Exception): pass
reference to a class or function within a module or module in a
package. If <name> contains a '{sep}', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
'symbols', 'specialnames', or 'modules', a listing of these things is
displayed.

{cmd} -k <keyword>
Search for a keyword in the synopsis lines of all available modules.
Expand Down
2 changes: 1 addition & 1 deletion Lib/pydoc_data/_pydoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ section section {
margin: 0.3em 0;
}

pre.topic, pre.keyword {
pre.topic, pre.keyword, pre.symbol {
background-color: var(--block-background-color);
border: 1px solid var(--block-border-color);
border-radius: 3px;
Expand Down
Loading
Loading