This is actually a very good question - first, let me explain why:
Guido himself (inventor of python) stated that if he could, he would take back the implementation of else, in fact, he openly admits he stole the else and elif concepts from C
https://twitter.com/gvanrossum/status/1142480012848717824
Historical concepts aside, lets look at why the else pattern is dirty in some cases and what we can do differently:
lets say we have a pattern where we are handling different situations in a logical cycle, for example perhaps we have an api that gets a post request, and depending on a type field passed to a filter, we might handle different cases:
lets first visualize an example of this with fastapi and a social media application in which we have posts, in this example we will have three primary types of post feed that we can access -
- trending
- following
- interests
@router.post(
"/get_post_feed",
response_model=schema.PostFeedResponse,
dependencies=[Depends(JWTBearer())],
)
async def get_post_feed(context: schema.GetFeed, request: Request):
"""
The get_post_feed function returns a list of posts that are sorted by the
time they were created. The function takes in a user id and an optional page
number and size to determine where to start the list of posts, as well as how
many posts should be returned. If no user id is provided, it returns all public
posts.
:param context:schema.GetFeed: Used to Pass in the filter type.
:param request:Request: Used to Get the user id of the logged in user.
:return: A response.
"""
user = request.user
content_filter: str = context.context.filter
response = None
if content_filter == "following":
response = get_latest_posts(user.id, context.params.page, context.params.size)
elif content_filter == "interests":
user_interest_list = [
x.asset_type_id for x in Interest.get_interests_by_user(user.id)
]
response = get_trending_posts_by_interest(
user.id, user_interest_list, context.params.page, context.params.size
)
elif content_filter == "trending":
response = get_trending_posts(
request.user.id, context.params.page, context.params.size
)
logger.info(f"response payload: {response}")
return response or {"items": [], "total": 0, "page": 1, "size": 1}
now lets define our interface models
class Post(BaseModel):
"""
It's a model that represents a post.
"""
id: UUID
content: str
created_at: datetime.datetime
post_likes: int
post_comments: int
liked: bool = Field(default=False)
class PostFeedResponse(BaseResponse):
"""
It's a model that represents a response from the feed endpoint.
"""
items: List[Post]
page: int
size: int
class Filter(BaseModel):
"""
It's a model that represents a filter.
"""
filter: str
class Config:
schema_extra = {"example": {"filter": "following"}}
class Params(BaseModel):
"""
It's a model that represents the parameters of the feed endpoint.
"""
page: int
size: int
class Config:
schema_extra = {"example": {"page": 1, "size": 3}}
class GetFeed(CamelModel):
"""
It's a model that represents the parameters of the feed endpoint.
"""
class Config:
schema_extra = {
"available choices": [
"trending: The top users trending",
"or",
"following: The top weighted people you follow",
"or",
"interests : The top assets you are interested in",
]
}
context: Filter
params: Params
great simple enough - we will skip the other functions like the db methods and such, they dont matter so much here - what matters is that we have three OR flows, only one can execute at a time. in the example provided we handle this with a switch-like method - elif
now, elif is not hacky, but its also not the best way to implement this, why?
if else if, else if is not necessary to achieve our flow - let me explain:
user = request.user
content_filter: str = context.context.filter
if content_filter == "following":
# explicitly return the value of the function rather than assigning it to a variable
return get_latest_posts(user.id, context.params.page, context.params.size)
if content_filter == "interests":
user_interest_list = [
x.asset_type_id for x in Interest.get_interests_by_user(user.id)
]
return get_trending_posts_by_interest(
user.id, user_interest_list, context.params.page, context.params.size
)
if content_filter == "trending":
return get_trending_posts(
request.user.id, context.params.page, context.params.size
)
# express the default case explicitly
return {"items": [], "total": 0, "page": 1, "size": 1}
in this example now, we have changed the code so that we are no longer assigning a variable, and actually break the request scope through explicit returns - in this case, we are narrowing the scope of the control flow to ensure less moving parts - notice how in the previous flow we had to log and capture the response object for analysis - this is not a bad practice, but an unnecessary one.
lastly, we have an explicit return value defined to handle what might have otherwise been an else case - that is to say, we know all of the possible rules of the flow and are able to plan around them.
this, more than anything, is expressly the point of why Guido said he would not have implemented else or elif - its not that they are not useful for flow control, its that as we plan our use case better we begin to realize that polymorphism and ambiguous use case are the real death of maintainability - by understanding all use cases and business rules before implementation, we gain the ability to simplify our flows and make our code more beautiful.
im not saying never use elif or else, im saying if you can not avoid their use, try to think bigger and ask why you need to handle abstract flow controls.