taleinat / python-stdlib-sentinels

Reference implementation of sentinels for the Python stdlib
MIT License
38 stars 5 forks source link

Style guide for sentinels #12

Open legendof-selda opened 1 year ago

legendof-selda commented 1 year ago

I propose a style guide when we create a sentinel. Common standard python sentinels like 'True', 'False', 'None' follow this style

  1. Naming Conventions: The requirement for sentinel names to be in title case and have descriptive names is clear and aligns with Python's naming conventions. This ensures that sentinel names are meaningful and self-explanatory. Example: Undefined, Missing.
  2. Avoiding Conflicts: The guideline to avoid overriding standard Python sentinels like None, True, and False is crucial to prevent confusion and unexpected behavior. It's important to emphasize that sentinels should not interfere with the behavior of built-in constants.
  3. Usage of Sentinels: The suggestion to use sentinels only when None cannot be used as a sentinel value is reasonable. It encourages developers to leverage the existing None sentinel when it fits the purpose as this is normal python standard.
  4. Complexity of Sentinels: The recommendation to keep sentinels simple and not create complex classes or subclasses aligns with the principle of keeping code straightforward and understandable.
  5. Consistency: Sentinels created must be consistent throughout the entire project. The created Sentinels must have the same meaning wherever it is used. The guideline to maintain consistency in the meaning of sentinels throughout a project is essential for code clarity and predictability.
  6. Sentinel Comparison Order: Sentinels cannot be compared with each other. Use Enums instead.
  7. Explicit Boolean Evaluation: Provide Boolean evaluation explicitly rather than setting it to be True by default as this avoids confusion.

    Example:

    
    Undefined = Sentinel('<undefined>')
    
    # Let's say a function can return `Undefined `
    value = foo()
    
    if value:
       print(f"Value received is {value}")
    else:
      raise ValueError("Value wan't available")

    Here you can see that, it makes more sense that Undefined was False rather than it being True by default. If a sentinel must be True or False depends on the context of the sentinel. Example: Undefined should be evaluated to False and Success can be evaluated to True, while EOF doesn't necessarily mean True or False and thus can have an "Invalid State" or it is "ambiguous". So, I propose that we should pass in explicitly if a Sentinel is truthy or not, using the truthy argument. The truthy argument can be True, False or None (default). If truthy is None, then we should raise a ValueError. This makes sentinels more explicit and can force the users to give meaning to a sentinel and enforces them to use is operator to evaluate.

    Example:

    Undefined = Sentinel('<undefined>', truthy=False)
    Success= Sentinel('<successful>', truthy=True)
    EOF= Sentinel('<undefined>')
    
    value = Undefined
    if value:
      print("value is defined.")
    
    value = Success
    
    if value:
      print("Function ran successfully")
    
    value = EOF
    
    # This results in a ValueError
    # bool(value)

    This makes the behavior of sentinels more explicit and avoids any ambiguity.

Can we discuss this and possibly add it into PEP 661 Proposal?

taleinat commented 1 month ago

Hi @legendof-selda,

Thank you for your thoughtful suggestions!

My apologies for not replying thus far. (I have not been able to do nearly any work on open-source things in the past year+.)

Regarding your points:

  1. Naming Conventions: Since the PEP aims to add a general solution for sentinel objects, which could be used in various use cases, I think that prescribing a naming convention would be counter-productive. I prefer to leave the naming to be done according to general naming conventions, and/or the conventions of the relevant code base, for better consistency. E.g., if a code base uses ALL_CAPS for constants, it may make sense for a constant sentinel value to use such naming.

  2. Avoiding Conflicts: This is important, but I see nothing special about sentinels in this regard; this is true for all variables and is already covered by existing style guides.

  3. Usage of Sentinels: Indeed, this should likely be included in the docs if and when this is added to the Python stdlib. (This is already clearly outlined in the rationale of the PEP.)

  4. Complexity of Sentinels: This is a nice idea; I'll consider including this as a recommendation.

  5. Consistency: Similar to point 2 above, this is a good general recommendation, but I can't think of a strong reason to point this out especially for sentinels.

  6. Sentinel Comparison Order: Good point! I'll certainly include this in the docs, and perhaps add it to the PEP as well.

  7. Explicit Boolean Evaluation: Making boolean evaluation (i.e. "truthiness checks") raise an exception by default would be too big of a deviation from existing common behavior: This almost never happens in Python. Therefore, I feel strongly that, by default, sentinels should evaluate to either True or False in a boolean context. Evaluating to True for sentinels other than None has a precedence: bool(Ellipsis) results in True.


I hope to complete this PEP soon, and welcome further discussion in the near future if you're interested.