Allowing Symbol Names to End with ? or ! in Python

This proposal suggests enabling Python developers to use ? or ! as optional suffixes in function and method names, similar to Ruby’s convention. This would provide a quick, visual indicator of functions that either return boolean values (with ?) or may raise exceptions or perform dangerous operations (with !). This optional syntactic enhancement can help developers understand a function’s behavior without always needing to check documentation or rely entirely on IDEs, especially in fast-paced or high-complexity codebases.

Motivation

In languages like Python, knowing whether a function might return None, raise exceptions, or require special handling typically requires checking documentation or relying on IDE hints. While well-documented code and good IDEs help, these systems are not infallible (for example some tools like Github can be hard to look up documentation)

Ruby addresses part of this by allowing ? and ! in method names, helping developers quickly recognize the method’s behavior:

  • ?: Indicates the method returns a boolean, such as empty?, valid?, etc.
  • !: Indicates the method performs a dangerous or mutative action or may raise an exception, such as save! or delete!.

In Python, adding this optional convention would enhance readability and provide developers with immediate cues about the function’s behavior.

Examples

Boolean-returning Methods (?) In Ruby, a method name like valid? instantly indicates that it returns True or False. In Python, we currently use names like is_valid(). The proposed change would allow:

def valid?():
 return some_condition

This immediately signals that the function returns a boolean value, without needing to look up its signature.

Dangerous or Exception-throwing Methods (!) A method that performs dangerous operations, mutates state, or can raise an exception could be named with ! to warn the user:

def save!():
 if not self.is_valid():
   raise Exception("Invalid state")
 # save logic

This would make it clear that save!() might raise an exception or require special care during use, as opposed to a safer save() method.

Benefits

  • Readability: Allows developers to infer a function’s behavior (boolean return type, possibility of raising exceptions, or dangerous operations) at a glance.
  • Code safety: Functions that mutate state, are unsafe, or throw exceptions can be clearly indicated with !, helping developers identify risks without needing extensive comments or documentation.
  • Compatibility with existing Python: This change is entirely optional and would not break existing code. It would be up to individual developers or teams to adopt the convention.

Cross-language Inspiration

Other languages already use naming conventions to make function signatures more self-explanatory:

  • Ruby: Methods ending with ? return booleans, and ! signifies mutative/dangerous operations or potential exceptions.
  • Swift: Function argument labels like with, for, etc., are required by the syntax, making function signatures more self-explanatory (e.g., func send(to recipient: String)).
  • Python: Though Python has keyword arguments that can be descriptive, it doesn’t offer similar syntactic cues in function names themselves.

Here’s an example of how the ? suffix could be applied to an attribute or method that checks for swear words in an object’s username and description, where the description attribute could be None.

Without the ? convention:

def contains_swear_words(user):
    if user.description is None:
        return has_swear_words(user.username)
    return has_swear_words(user.username) or has_swear_words(user.description)

With the ? convention on the description attribute:

def contains_swear_words(user):
    if user.description? is None:
        return has_swear_words(user.username)
    return has_swear_words(user.username) or has_swear_words(user.description?)

Here, the description? attribute signals that the description can be None, making it clearer to anyone reading the code that this value needs special handling. The ? suffix on description? indicates that the attribute might not be present, or could hold a None value, thereby prompting developers to handle it more cautiously.

Conclusion

This optional enhancement would give Python developers more expressive power to signal important aspects of function behavior (returning None, exception raising, mutative operations) directly in the function name. While the change won’t replace good documentation or IDE support, it would provide an additional layer of clarity and code safety.

This change can help avoid certain common errors or misunderstandings, such as forgetting that a function might return None, raise exceptions, or mutate an object. Although it’s a subtle enhancement, it has the potential to improve readability, maintainability, and robustness in Python codebases.

4 Likes
  1. as a matter of practice All functions can raise in python.
def foo():
    while True:
        pass

This function can raise. ! doesn’t help here. Read the docs for things you should expect and handle them. Any errors raised from this example in particular are probably not your problem. Document errors you raise for others too.

  1. ? isn’t needed for that, we already have type annotations which cover return types, and which are much more powerful for type safety than just a visual indicator as a convention
9 Likes
  1. “All functions can raise in Python”:

While it’s true that technically any function in Python can raise an exception, the use of ! isn’t intended to label functions based on the fact that an exception could occur. The ! convention is more about intentional signaling of functions that are designed to raise exceptions as part of their normal behavior or perform dangerous, mutative operations. For example:

  • A function like save!() might throw an exception if data validation fails, making it clear to the developer that this action needs careful handling.
  • This convention provides an explicit warning of elevated risk, similar to marking a function as unsafe in Rust.

While theoretically every function could raise an exception, the ! suffix would target common or likely failure points, where the developer should take special care.

  1. Documentation and Readability:

The point about reading the documentation is valid, but the purpose of using symbols like ? and ! is to help reduce the need for constant back-and-forth between code and docs in the first place. While reading documentation is essential, certain patterns in function names can improve code readability and reduce human error:

  • Python already uses naming conventions to avoid frequent doc lookups. For instance, using keyword arguments and descriptive names reduces ambiguity. Similarly, Swift uses argument labels to make function calls more self-explanatory without reading docs. This kind of inline signaling (whether via keywords or naming conventions) helps make code more intuitive.
  • Type annotations indeed help with return types, but the ? convention adds additional semantic meaning at the call site

Without needing to inspect the type annotations, the developer instantly knows this function is a boolean check based on the ?, rather than having to rely on additional inspection tools or documentation.

  1. Type Annotations:

Type annotations are a useful feature in Python to clarify the expected types of inputs and outputs. However:

  • Not all codebases or projects enforce type annotations rigorously. While the Python community is increasingly adopting types, this is still optional, and a large portion of legacy or untyped code may not provide this information.
  • Type annotations also don’t provide contextual or behavioral cues. For example, a function could return Optional[bool], but it wouldn’t tell you that None has a special significance, or that the function may check a condition in a way that requires extra attention (which ? would hint at).
  1. Balancing Flexibility and Explicitness:

Adding a ! or ? is not about replacing documentation or type hints but adding optional explicit cues that enhance code readability. This can be especially useful in larger codebases or when collaborating with multiple developers. While experienced developers might say “just read the docs,” in real-world scenarios, even well-documented code can be prone to misinterpretation or missed details during time-sensitive work.

  1. Docs Aren’t Always Easily Available:

While the ideal is to always read the documentation, in practice, this isn’t always feasible. For instance, during code reviews, developers often rely on tools that may not have the same functionality as an IDE—such as code completion, symbol lookup, or easy access to documentation. In these cases, conventions like ! or ? can provide immediate context about a function’s behavior without needing to leave the review tool, reducing back-and-forth interruptions.

Additionally, third-party libraries may not always have thorough documentation, or you might be working in environments where documentation isn’t up to date. These naming conventions can give developers more self-contained clues about the function or method.

  1. Optional and Developer-Driven:

The proposal doesn’t force this convention on anyone. It’s a completely optional enhancement, meaning that developers who see value in it can adopt it, while others are free to continue with their preferred style. This flexibility ensures that different teams or projects can decide what fits their workflow best, while allowing those who prefer extra readability cues to use them where it makes sense.

In summary, the ! and ? naming conventions are not a substitute for documentation or type annotations but an additional layer of safety and clarity. They can reduce the cognitive load on developers, especially when juggling multiple components, allowing them to infer function behavior and risks at a glance.

1 Like

It seems to me like this is covered by doc strings and type hints already.

def foo() -> bool:
    return condition

def bar(s: str) -> str:
    """
    This does something. Maybe.

    :param s: String to process
    :return: Processed string
    :raises: TypeError if s is not a string
    """
    if not isinstance(s, str):
        raise TypeError(f'A string must be passed not {type(s)}
    <insert processing here>

I don’t see the added value in adding ? or ! to the function name when those indicators exist.

6 Likes

No, it would not. Line-noise is not easily readable.

Please stop trying to turn Python into Rust.

15 Likes

if it’s optional, then this is literally just noise. it creates a situation where the absence of it doesn’t mean anything, so putting it here doesn’t help. you always have to check anyhow.

5 Likes

One of the perks of Pytho, is that it reads like english, like if a not in b. So the is_ prefix convention is more coherent and readable that a question mark.

There are other potential uses for question marks (see the thread about None-aware operators), it would be a shame to “spend” it on a mere convention.

As for !, it would conflict with !=: what would mean save!=foo, is it an assignment to save! or a comparison between save and foo ?

7 Likes

Ruby’s so-called “dangerous methods” are annotating mutation of state, not “danger you might have to be careful”.

I would actually really like having a tidy way to notate that, especially in code which handles both mutable and immutable objects. (Hint: that’s actually all nontrivial Python code…)

So even if we did add ! to identifier chars, which is problematic as it stands, if I saw a suffixed ! in a method name I would assume it’s a mutator!!


Special symbols which are easy to type on most international keyboard layouts are precious. They shouldn’t be spent carelessly in language design.

Ruby has a very different style and parsing from Python, which is part of why their conventions work for them.


This proposal gives me a strong vibe (and it is just a vibe, so I could be wrong) that you aren’t enjoying the degree to which Python relies on documentation.

I don’t even disagree, at some level! Reading docs is hard. Sometimes you’re reading a doc about something straightforward like adding an element to a list and then the doc starts telling you about the Liskov Substitution Principle and you spend a whole afternoon learning that you have to pass a float, not an int!

But… We aren’t going to eliminate the need to read docs, ever. Nor should we even want to or try to.
Plus, I continue to benefit from having learned about LSP and type variance, so that was an afternoon well spent.


Here’s a real enough snippet to think about:

def prompt(p):
    try:
        return input(p)
    except KeyboardInterrupt:
        return None

Is it correct?
Well, it doesn’t handle EOFError. Should it?

What about this version?

def prompt(p):
   """
    Prompt for user input 

    returns None if stdin is closed or the user hits ^C
    """
    try:
        return input(p)
    except (KeyboardInterrupt, EOFError):
        return None

And this?

def prompt(p):
   """
    Prompt for user input 

    returns None if the user hits ^C
    """
    try:
        return input(p)
    except KeyboardInterrupt:
        return None

Finally, this?

def prompt(p):
   """
    Prompt for user input 

    returns None if the user hits ^C

    Raises EOFError if stdin is closed
    """
    try:
        return input(p)
    except KeyboardInterrupt:
        return None

So, what does this tell us? Basically, if it’s sane and well enough written, reading the docs can tell you what to watch out for. I don’t see how notation of some of these as “dangerous” would help.

1 Like

Yeah I will admit I don’t particularly love the state of documentation with Python but it’s also a personal preference

I strongly believe in self documenting code there are many contexts where it may not be easy/possible to pull up the documentation or you simply forget certain details around an API even if you have read the docs

Regarding your example I wouldn’t consider that a case where we would have “!”

I see this being more in line with some languages that make a distinction between Exceptions they consider common, usually more logic based and should be handled at the earliest point where it makes sense to deal with it to avoid invalid data propagating

And errors that are rare, more structural in nature and probably better handled at the most high level place that has the context needed to deal with them

For example

from PIL import Image

img = Image.open!(file)
img.rotate()

Both of these methods of course can throw exceptions, but open in particularly is the one most likely to fail seeing as it will have common issues such as file not found. It makes sense to highlight to developer to handle this case as soon as possible so they build reliable apps

Rotate may throw an exception for example “out of memory” but these errors are potentially less likely on the whole and in addition the kind that need to be handled at a much higher level in the application (maybe there is a something somewhere that manages the apps resources)

But yeah this feels like I’m now making the case for some kind of syntax for making sure people handle these common cases rather than adding “!”

1 Like

Letting exceptions escape unhandled by default is intentional. For developer-facing scripts, a traceback pointing to the line that failed is often entirely adequate error reporting - it’s only when you want to make something usable by folks that aren’t interested in the internal details of the code being executed that you need to start improving the error reporting UX.

Adding a helper context manager to argparse that turned unhandled exceptions into a nice usage error report (similar to argument parsing failures) instead of a sprawling traceback could be a genuinely nice enhancement on that front, but having random ? and ! symbols scattered through code wouldn’t help (especially since ~35 years worth of pre-existing code wouldn’t feature those markers).

For this kind of semantic marker to be genuinely effective, it needs to be present from early in a language’s lifecycle, allowing the relevant API design conventions to develop around them (as is the case in Ruby - attribute names have never been tightly restricted there, allowing the ?/! suffix conventions to develop over time).

Python’s conventions in this space are different. ? methods and properties usually get names that read like a meaningful English phrase when preceded by if (as in your own if self.is_valid() example). Dangerous APIs are usually at least given a _ prefix to suggest they shouldn’t be used externally, and may also be given an _unguarded or _unsafe suffix to remind even internal users to be cautious in how they make use of them.

(In regard to ? specifically for typehinted APIs, the -> bool return type in boolean query methods and properties will also be shown in the identifier hover tooltip in most modern Python IDEs)

The other key issue with this idea is that using ? and ! for this purpose is likely to rule out any other potential uses.

For ?, Python doesn’t require spaces around operators:

>>> a = b = 1
>>> a+b
2

This means permitting ? to appear as part of identifiers pretty much also rules out any possibility of ever using that symbol as part of any operators. While the odds of us ever adding the None-aware operators defined in PEP 505 aren’t particularly high, they’re still a more potentially interesting use case for a trailing ? following an identifier than this would be.

Similarly, a trailing ! following an identifier already has a tentatively proposed meaning in PEP 638 – Syntactic Macros | peps.python.org, which would allow user-defined syntactic constructs like:

from! declarations import bijection

bijection! color_to_code, code_to_color:
    "red" = 1
    "blue" = 2
    "green" = 3

which would then translate to the following pair of dictionaries at compile time:

color_to_code = {
    "red": 1,
    "blue": 2,
    "green": 3,
}

code_to_color = {
    1: "red",
    2: "blue",
    3: "green",
}

While it’s far from a given that syntactic macros will ever be added to the language, the possibility of doing so sets a very high bar for proposals that want to suggest unrelated syntactic use cases for the ! symbol.

11 Likes

If we were to add syntactical ? to the language I’d much rather have something like JS optional chaining operator than this redundant Ruby-ism

1 Like

Personally, I would like to use ! and ? in function and method names.
If it was possible, I would use names ending in ! for all functions that mutate their arguments, and all methods that mutate state.
And this proposal would allow me to do that.

However, I expect the devs will want to reserve ! and ? for future use. And I can see the argument that keeping ! in reserve for things like macros and safe navigation is worth more than the short-term convenience of having ! in function names.

One big question is whether to rename/alias existing library functions and methods to adopt this new naming convention.

Renaming would cause great backwards incompatibility. Aliasing would generate significant code churns. Not doing anything at all to existing functions would make the new convention, well, not a convention.

2 Likes

A project I worked on a while ago involved wrapping Ruby APIs with
Python, and it would have been handy to be able to use them.

I wouldn’t advocate adding them to Python just for that, though.
Identifiers containing punctuation chararcters other than _ don’t seem
to fit the style of the language.

3 Likes

To be clear, the proposal is that the definition and the invocation and definition must match exactly? i.e.

def write!():
    ...

write()

would be a name error? There would be no name normalisation?

If that’s the case then I suspect I’ll be constantly misguessing/misremembering which functions have punctuation suffixes, which libraries follow which conventions for what ! and ? signify and which functions within those libraries don’t follow their own conventions due to either backwards compatibility constraints or human error.

You can already use the Lisp convention of a suffixed P (for “predicate”) without changing the grammar.

2 Likes