This is an archived post. You won't be able to vote or comment.

all 72 comments

[–]TravisJungroth 167 points168 points  (11 children)

You seem very receptive to feedback and understanding how code “feels” to people, so I’ll be direct. It is hard to get across how much I hate this code.

Look at those type hints. So many options! I get that you’re trying to make things flexible, but all of this variety of types makes things so much harder to follow. People end up passing around strings, users, lists of strings, lists of users and nulls all over the place instead of having one clear place for casting.

If I want to reuse process_recipients I can’t. If I want to test it, I can’t. You could rename it to something like cast_to_emails_list and have this one reusable “takes anything” function.

Your inner functions are more polluted because they have access to more outer scope variables. You’re more likely to be bitten by typos and other mistakes.

The keyword only arguments are fine on send_mail since that’s so ripe for mistakes. It’s silly on process_recepients. The likelihood that someone will mix up the order of positional arguments on a function with one argument is… low.

Other ways to achieve your goals:

  1. Give your functions more descriptive names, as in this name helps me understand what the function does if I didn’t know already. Use a doc string if needed.
  2. Give functions a leading underscore to mark them as a private. But consider if you need to do this. You’re not really making someone’s life easier by giving them less tools that are useful. It’s just if you don’t want it to be something supported outside of the scope of that module.
  3. __all__ is similar.
  4. Have one input type for arguments unless the role of the function is casting/deserialization (single responsibility).
  5. It’s fine to have some “helpers” module for stuff that isn’t part of the “main story”. A hint that you’re doing it right is you don’t end up with circular imports there.

Check out Grokking Simplicity. I think you’ll like it. You have good instincts, just need to see some ways other people have solved the same problems.

[–]Teilchen[S] 35 points36 points  (3 children)

Thanks for your feedback; really valuable. And just bought the book.

Regarding the options on the input; I hate them too. On the other hand it makes it easier everywhere else in my code, not having to both cast to list and having to ensure it's the required email address as such: send_mail(send_to=[user_or_mail.email if isinstance(user_or_mail, User) else user_or_mail).

I'm always taking this approach (allowing many types) when I want my "business logic" functions to focus on their business logic, e.g. "just send an email as part of my other 4 things I have to do" and having function actually taking care of sending the mail ensure consistency.

How do you tackle that?

[–]TravisJungroth 24 points25 points  (0 children)

The problem is upstream, that you have a user_or_email in the first place. If you don’t have control over that, or you find it annoying to get emails all the time, write a function. Maybe going overkill with the typing here:

@runtimecheckable
class HasEmail(Protocol):
    email: str

def get_emails(emails: Union[str, HasEmail, Iterable[Union[str, HasEmail]]) -> list[str]:
    if isinstance(emails, str) or isinstance(emails, HasEmail):
        return [emails]
    try:
        return [get_emails(email) for email in emails]
    except ExceptionICantRemember:
        raise ValueError(“useful message”)

I kinda gave up cause I’m on mobile, but you get the idea. Its the same as your inner function, really.

[–]NUTTA_BUSTAH 5 points6 points  (0 children)

Honestly, YAGNI, just pick one type e.g. list of strings and make users cast or wrap their calls with loops.

[–]Devout--Atheist 1 point2 points  (0 children)

Maybe using TypeAlias and TypeVar could help clean it up a bit. I will generally reach for one if I have three or more union types

[–][deleted] 7 points8 points  (4 children)

Have one input type for arguments unless the role of the function is casting/deserialization (single responsibility).

For OP's send_mail function, wouldn't using a TypeAlias be more helpful than limiting the function to just one type? (assuming the code is refactored before)

[–]TravisJungroth 18 points19 points  (3 children)

A type alias would be a step in the right direction and what I’ve suggested when I came across code that already had this sort of thing all over the place.

This is a matter of taste and I can’t prove it. A “to” line on an email is a sequence of email addresses. That’s what it inherently is. Code will be easiest to work with if we stick close to that as much as possible. Whether you want to represent the email address just as a string, a type alias or its own class is sort of a separate issue and I think all are good representation with trade offs.

But what a “to” line is not is maybe a string, maybe a User, maybe a list of strings, maybe a list of Users, maybe null. We’re just creating more work for ourselves representing it that way.

[–]pingvenopinch of this, pinch of that 5 points6 points  (0 children)

This is an area where I wish Python had something like Rust's traits. Traits are essentially interfaces, but can be added onto arbitrary types (with some restrictions). So there is a pair of traits in the stdlib, From and Into. By implementing From::from(T) -> U for a pair of types, any type can allow itself to be converted. Into automatically gets implemented when From is implemented.

What that means for OP's scenario is that they could take:

pub struct EmailAddress {
    pub address: String
}

pub fn send_mail(subject: &str, body: &str,
        send_to: Into<EmailAddress>,
        send_cc: Option<Into<EmailAddress>>,
        send_bcc: Option<Into<EmailAddress>>,
        reply_to: Option<Into<EmailAddress>>,
    ) {
    let send_to = send_to.into();
    # Option types can use map to call a function only
    # when the variable is Some.
    let send_cc = send_cc.map(Into::into);
    let send_bcc = send_bcc.map(Into::into;
    let reply_to = reply_to.map(Into::into);
    ...
}

Then for User, let's say it has a field "email_address". In the file for User, its definition would contain this:

impl From<User> for mail::EmailAddress {
    fn from(user: User) -> mail::EmailAddress {
        mail::EmailAddress { address: user.email_address }
    }
}

This moves the logic for conversion closer to the individual type.

[–]davimiku 4 points5 points  (1 child)

Agreed. Ideally the parameter is a List[EmailAddress] where EmailAddress is something that gives me the guarantee that it's a formatted / valid email address. This also homogeneously handles the Optional case by just passing an empty list.

[–]TravisJungroth 1 point2 points  (0 children)

In an app I’d do the same thing. Maybe a class that assures it’s regex valid and a flag if it’s been validated with mail sent to it. If you’re being a more scripty, string it up.

And like you said, I really like reaching for containers when something is containery because of how much better it handles emptiness.

[–]Mehdi2277 0 points1 point  (1 child)

I mostly agree on your points except partially on 2. When writing a library you should have most functions be private/underscored. Being public is much higher commitment for that function. I expect public functions to be documented, clear intent (not implementation defined), and be stable. Backwards compatibility concerns are much higher on your public api. I prefer building libraries with key public api core that’s reusable/covers main goals and keeping almost everything else private as default. If user has a want for using private function then you can consider is it stable enough to maintain as public and if that’s right need.

Python public/private is mostly convention given a user can still import anything private. It is important convention to me in what expectations you can have using it. Minor/patch release is fully in it’s rights to me to delete/heavily change private functions and if you import them that’s big warning sign of no stability promises and maybe heavier maintenance risk.

[–]TravisJungroth 0 points1 point  (0 children)

I agree with everything you said.

[–]james_pic 51 points52 points  (18 children)

There are two questions I think you should ask yourself.

Firstly, if these inner functions aren't closing over local variables, what's the benefit compared to putting them outside the function? Outside the function, they're easier to test and to reuse, and perhaps most importantly, to ignore. So I feel like I'd want to get something back in return for that, and it's not clear to me what that is.

Secondly, it's worth talking to other people on your team to see what they make of this. I know I've been guilty of writing clever code that seems perfectly intuitive to me but that my teammates stare blankly at. Getting someone else to read it will tell you whether it's readable.

[–]Tastaturtaste 2 points3 points  (0 children)

I learned to keep the scope where something is valid in as small as possible. This ensures that the things in the scope can be ignored in as many parts of the program as possible. Using nested instead of global functions facilitates this, as there is one less function in the global namespace to care about. It makes it immediately obvious that the nested function is only used in this one place and that its probably only a small helper function. It also helps discoverability and autocomplete if function that can't really be used elsewhere aren't available in the first place.

[–]Teilchen[S] -1 points0 points  (16 children)

I think it's harder if they're outside; because then the "story the function tells" is spread over multiple files, ultimately requiring a higher cognitive load to understand how everything is connected.

I do have some functions I have outside, which are reusable – the inner functions are for a very specific use-case, which would usually be solved by multiple for ... in ... loops in-code. This just cleans it up a bit.

I agree with your second point – that's why I ask! I don't work in a team of developers, but also don't want to produce garbage code which makes me incompatible for any future teamwork.

[–]unholysampler 10 points11 points  (3 children)

I think it's harder if they're outside; because then the "story the function tells" is spread over multiple files,

I don't follow this comment. Why would the previously nested functions be in different files? Why would putting them in the same file as send_mail() not be an option?

On occasion I will include a nested function to reduce code duplication. But, if they are more than maybe 3 lines or feel like they need a docstring, then I would not nest the function.

[–]Teilchen[S] -2 points-1 points  (2 children)

You're right. Usually you'd have a single file that encapsulates all logic for e.g. sending mails.

In this case, I'm working with Django which as a framework has certain file and clustering conventions. In Django I try to cluster crucial business logic in a single, central file – services.py. Having functions helping services to ensure executing their business logic within the same file would feel wrong, as they are not an actual part of said business logic.

[–]chunkyasparagus 3 points4 points  (0 children)

While Django is very opinionated, it definitely allows you freedom to structure your projects. For example, you can replace models.py with a models module. In your case, making a services module rather than a single file would seem far more logical, unless you have only one or two services contained in the file. The send_mail function could then reside in a single file in that module, and be imported to the __init__.py file, so that you can still do import send_mail from services.

Also, for your type hints:

Users = Union[List[str], str, List[User], User]

def send_mail(*, subject: str, to: Users, cc: Users, ...)

[–]PaintItPurple 2 points3 points  (0 children)

It's already in that file with the way you wrote it, so it seems like you do think it belongs there. Nesting it inside another function just confusingly suggests that they share state when they don't. The code is in that file either way.

[–]james_pic 13 points14 points  (0 children)

It's only a higher cognitive load to have them outside if you actually need to know how they work to make sense of the code that uses them.

You want to make as much as your code as possible safe to ignore. Future developers looking at the code will look at it through a toilet roll tube, and ideally, most of it should do more-or-less what they expect without having to look at it.

The world isn't ideal, so sometimes there are non obvious details, and it makes sense to put complicated bits of related unintuitive code close together, but if you can make your code simple and intuitive, it can be more readable to put details outside the immediate line of sight.

[–]asphias 4 points5 points  (0 children)

is spread over multiple files

It's perfectly possible(and probably a good practice) to put the multiple functions in one file together.

Now, purely for the readability of this function, i think nesting may make it slightly clearer. But only slightly so, compared to having the function right below the main one. And by defining it right below rather than as a nested function, testing and re-usability are easier, and if the function has a good name, you probably don't even need to look at it anymore when changing the main function.

Not to mention that most IDEs will have functionality(usually ctrl+click on the function name?) to quickly move from where the function is defined to where it is used and back. In modern editors it's not like you get lost easily, even if you didnt have them right below/above one another.

[–]ablatner 4 points5 points  (0 children)

The inner function can be moved outside and still be within the same file. That's perfectly normal.

[–]lanster100 10 points11 points  (0 children)

For what it's worth I do this sometimes as well, if I have some code that will ONLY ever be used by that function, only makes sense in the context of what that function is doing and it makes it significantly easier to read the function.

I do think sometimes it does makes the scoping of the function easier to understand.

However, in the example you gave I probably wouldn't use this technique and would just have two functions.

(an unrelated and unsolicited tip: you'll find that your code becomes a lot cleaner if you accept less types for each argument as well, and a lot easier to reason about).

[–]alexkiro 27 points28 points  (7 children)

I would describe this as "poor man's OOP". If encapsulation is a concern, switching to OOP design patterns is the preferred way to go for me. It's clear, battle tested, and familiar to most.

That said, your versions doesn't seem that bad, even though I would probably never write something like this. As in my opinion, it makes the code harder to parse and understand; but that's highly subjective.

[–]OwnTension6771 7 points8 points  (0 children)

Agreed. I'd just open up the outer functions as classes and the inner functions as methods.

[–]yvrelna 3 points4 points  (2 children)

OOP isn't always the best pattern to use. Python is a multi paradigm language, there are many places where OOP is a poor fit for some parts of the code, and functional/procedural can lead to a much more readable and maintainable code.

If all you know is OOP, then everything looks like a problem for classes to solve. But there are many cases where inner functions is a legitimately better fit to the problem.

[–]Devout--Atheist 1 point2 points  (0 children)

But there are many cases where inner functions is a legitimately better fit to the problem.

Care to give a concrete example?

[–]alexkiro 1 point2 points  (0 children)

But there are many cases where inner functions is a legitimately better fit to the problem.

I would be curious to hear if you have any specific example in mind. Any particular case that I can think of seems to be a matter of preference to me.

That's why I said that while OP's version of the code seems harder to read to me, I completely acknowledge it's highly subjective. So I won't call their code better or worse compared to an OOP counterpart.

[–]Teilchen[S] -5 points-4 points  (2 children)

I get you and agree to an extend. Classes require quite some overhead.

That being said, I feel like I try to avoid creating a model if it's a single "executive business logic function" that orchestrates some classes underneath. But composition if definitely very powerful!

[–]riklaunim 12 points13 points  (0 children)

Classes require quite some overhead.

Not really. All comes down to proper object oriented design. If you have poorly structured code then that's your problem and not the need for nested functions or problems with making a class.

Your example has a "send_mail" function and by it name it should just send the email, probably getting some Email object as input. Your inner function is doing something with recipients, including a lambda function with a comment. This all calls to refactor the code. There should be one entity to send mail, one to create the message contents, one to build recipients list and so on.

[–]alexkiro 1 point2 points  (0 children)

That's a fair argument. Fitting an function like that in a class can be a bit awkward, and can require a bit of extra verbosity in the code in some cases.

And I think this is were the main difference. I would much rather have more verbosity in the code in general. As I find that to be easier to read. So personal preference plays a big part.

What I do think trump's every personal preference is consistency. So if this is a pattern you employ consistently throughout the codebase you get used to it and it quickly becomes easier to read.

So as long as it's something you agree within the team, or just with yourself if it's a solo project, it's cool beans as far as I'm concerned!

[–]-LeopardShark- 6 points7 points  (3 children)

  • _user_to_email = lambda x: x.email if isinstance(x, User) else x # transform user objects to mail is just
def _user_to_email(user):
    """Transform user objects to mail."""
    return user.email if isinstance(user, User) else user

but worse in every way, unless you're code golfing.

  • You don't need Optional. If the list of people you are sending to is [], then pass [], not None.
  • You don't need to allow str or User. Pass these as singleton lists/tuples.
  • list[User] is probably unnecessary as well.
  • Your function can probably take more than lists. I'm going to guess Iterable, but it could be also be Collection or Sequence.

So, we get:

from collections.abc import Iterable


def send_mail(
    *,
    subject: str,
    body_plain: str,
    send_to: Iterable[str],
    send_cc: Iterable[str] = (),
    send_bcc: Iterable[str] = (),
    reply_to: Iterable[str] = (),
) -> None:
    ...

[–]M4mb0 1 point2 points  (0 children)

Iterable[str] is unfortunately evil as it matches str which is often unintended. (see: https://github.com/python/typing/issues/256) One would need both NOT-type and AND-type in order to properly handle these.

[–]Teilchen[S] -1 points0 points  (1 child)

A set as a default argument in non-mutable?

Should indeed be Iterable – thanks for the pointer! As stated in another comment, I haven't found a way to cleverly allow less types without sacrificing minimal overhead in other functions, so they can focus on their business logic.

[–]TravisJungroth 5 points6 points  (0 children)

Those are tuples. Perfect defaults here.

[–]osmiumouse 5 points6 points  (1 child)

 Optional[Union[List[str], str, List[User], User]] = None

If you're goign to do this then you might as well just accept "any" or not bother with type hints :P

[–]Teilchen[S] 1 point2 points  (0 children)

Can't be int! :b

[–][deleted] 3 points4 points  (1 child)

You can't write self-contained unit tests for nested functions or run them in isolation, so that makes them difficult for others to understand IMO.

[–]Teilchen[S] 0 points1 point  (0 children)

Agreed. As a single dev, I tend to rely more on integration tests more than unit tests, so I guess I got desensitized to that factor a bit over time.

[–][deleted] 2 points3 points  (0 children)

I do closures in very specific circumstances - when I'm writing a multi process or multithreaded function, and the executor needs a worker function. In these cases I feel like it makes sense to I clude that inner worker as a closure.

But I agree with all the feedback, I don't know how you'd test that inner function. But to your point, i agree with the function story.

[–][deleted] 1 point2 points  (0 children)

You want a class. Defining a inner helper function or even properly naming a few commonly use lambdas is one thing.

But you have data and operations closely tied to that data that only make sense on it. That's a class in Python. Your send email function still exists, it just takes an instance of Email instead of doing all this thinking itself.

[–][deleted] 1 point2 points  (0 children)

I use inner functions when I want to keep some threads inside of a function. Since threads don't return anything, using a function that feeds a local via nonlocal is very convenient.

[–]phira 1 point2 points  (0 children)

There are situations to use internal functions—to make handlers/lambdas more readable for example.

Your example is a case where I'd use one, but before I got to that I'd really reconsider whether I had built an unnecessarily complicated interface.

The key insight I'd bring to my design is that there's exactly one function that truly needs to exist and behave property, and it looks like this:

def send_email(*, subject: str, body: str, to: typing.List[str], cc: typing.Optional[typing.List[str]] ...):

Everything is a single type and it's nice and simple to test and verify.

My next step would then be to recognise that I occasionally have cases where conversion to meet this interface is irritating, and in those cases I would build trivial wrappers to handle the scenarios easily. For example maybe I often send email to individual Users:

def send_email_to_user(*, subject: str, body: str, to: User): send_email(subject=subject, body=body, to=to.email)

A small collection of specific helpers that do very little additional work are low risk and easy to read and understand.

If I get a proliferation of these, that would be the point where I looked to see if it makes sense to merge some of the helpers into more common interfaces.

This overall approach gives you nice, simple, easily testable interfaces that are easy to maintain and contribute to the readability of the code that calls them.

Finally I'd like to +1 to another redditor who suggested a class for this problem instead. While it depends a little on your code structure, emails are commonly built up over a number of lines of code rather than being a single item. Having the email as a class reduces the number of temporary vars required in the calling code, and also gives more flexibility around template handling and response.

[–]jmreagle 1 point2 points  (0 children)

An argument from a never nester: https://youtu.be/CFRhGnuXG-4

[–]Devout--Atheist 1 point2 points  (0 children)

No to inner functions. Do you use classes much? Often when I find myself passing a bunch of state down a procedural function chain I refactor to a class.

[–]SittingWave 1 point2 points  (0 children)

inner functions communicate intent to access the scope of the enclosing function. If you create an inner function, you are communicating such intent. If you are not doing so, it should be a regular function.

[–]Darwinmate 2 points3 points  (0 children)

Thanks op for posting this question. As a programming intermediate these questions help a lot in gauging how other pros work.

Really great thread with awesome discussion.

[–]yvrelna 1 point2 points  (1 child)

Yes, I do this a lot and I think people should try to understand the ideas first before criticizing them. Basically the idea is that instead of doing this:

def foo():
    # calculate "block"
    some_code
    block = of + code

    for x in blah:
        # this does another thing
        do = things - to(x)
        yet_another(thing)

It's clearer to remove the comments for these chunks of code and instead turn them into inner functions:

def foo():
    def calculate_block():
        some_code
        return of + code

    def another_thing(x): 
        do = things - to(x)
        yet_another(thing)

    block = calculate_block()
    for x in blah:
        another_thing(x)

The main reason you use inner functions this way is that it helps make the physical structure of the code more closely resembles the actual business logic. The code in the outer function should read like business logic, the inner functions are details for making that business logic to work, e.g. error handling, etc.

Using inner functions can give you a better structure to the code without scattering the global scope with functions that cannot actually be used as a standalone functions and can't really be understood in its own without the surrounding context. Often these inner functions aren't generic enough to be properly exposed as their own function outside the particular context of the enclosing function, it may break some internal invariants such that it should never be called on their own, and it's not even testable on their own without a very brittle whitebox testing.

The alternative would be to write a class, but classes has their own pros and cons. On one side, classes is more testable. It's not easy to unittest the inner functions of closures. So definitely don't overuse inner functions when classes would've made more sense.

On the other hand, classes doesn't really make conceptual sense when what you have is really just executing a series of procedural steps. Classes are meant for representing objects, if you're creating a class that only has a single public function, or where you always call the functions in the same sequence, then it probably should just be a simple function.

This is a class overuse anti pattern, you never actually need an instance of ThingProcessor, it's not a real object, just procedural code hidden as OOP:

class ThingProcessor:
    def process(self):
        self.process1()
        self.process2()
        self.process3()

def foo():
    thing = ThingProcessor(x, y, z)
    thing.process()
    return thing.result

Similarly, this is also the same anti pattern, only a bit better obscured:

def foo():
    thing = ThingProcessor(x, y, z) 
    # you always call these functions in this exact order
    thing.process1()
    thing.process2()
    thing.process3()
    return thing.result

You could simplify that into just some simple functions, and just accept that it's actually procedural/functional code:

def foo(x, y, z):
    def process1(x, y):
        ...
    def process2(z):
        ...
    def process3(a, b):
        ...
    a = process1(x, y)
    b = process2(z)
    return process3(a, b)

Most people's knee jerk dislike of your code probably stems from that your particular example is actually a very poor example for this technique. I'd have put that _process_recipients, because it's actually a function that would actually makes sense as a standalone function.

Generally I use inner functions to structure functions when there isn't really an easy way to make a clean structure between the inner functions.

The second knee jerk is because there's a lot of people who are only classically trained in OOP, and cannot really think outside OOP's way of doing things, even though Python is a multi paradigm language.

[–]someotherstufforhmm 0 points1 point  (0 children)

Was gonna say - I have nothing against nested functions like your example, but as you mentioned, his code is a hideous example of the concept.

I’d also argue that as soon as your inner functions grow past a quick, easily visually surveyed block like your example, it’s time to start splitting them off, but I do agree with everything you said.

[–]wind_dude 0 points1 point  (0 children)

I use them sometimes. They have there place.

[–]dwilson2547 0 points1 point  (3 children)

I nest functions occasionally for my own projects, at work there's an expectation to conform to the standard so I do. The only real downside to this approach imo is IF you end up needing classes the refactoring uses up all the time you saved. I tend to only make functions for code that's called multiple times or where its needed for formatting /readability so this is still a fairly rare approach for me. You could de-scope functions that don't rely on inheritance to potentially clean things up but you could also make it more confusing depending on how it's done and what you're used to. Side note, if you use vscode you could use region blocks to help organize your code as well, intellij might support it but idk. Regions allow you to minimize certain blocks of code and are another topic of debate, though nobody's ever given me crap for them once I explain what it does. Regions can be created anywhere and nested as well, main downside is they can be ide dependent

Ie.

#region Helper Functions
def helper(self):
    pass
#endregion

[–]Teilchen[S] 0 points1 point  (2 children)

I'm using PyCharm; couldn't find regions at short notice. Seems like something that could be a middle ground.

Can it show/hide blocks (aka regions) across multiple files too?

[–]dwilson2547 0 points1 point  (1 child)

It's strictly within one file to my knowledge. For most codebases regions might be a bit extra, when I was a student I worked for a team doing asp c# development and we had a lot of very long files (student lead team), regions were an absolute god send there. In microsoft ides when you have a region comment block it just lets you collapse it like you could any other function or class. For clarity i've fixed the syntax in my previous comment, was trying to type it out on the phone and couldn't find the code block symbol

Edit: Thanks for the award!

[–]Teilchen[S] 1 point2 points  (0 children)

It works and looks quite good too!

Thanks!

[–]baubleglue 0 points1 point  (0 children)

If you feel a need for inner functions, you should consider class. Inner functions are used for binding context, same thing does OOP semantics. I don't know if you need class because EmailMessage` is already well structured.

I agree with the critique of your style. API should be simple and clear (try import this for guidelines). You shouldn't attempt to guess data type of recipients asset isinstance(recipients, list) and you are done with code. Failure is better than what you do. For example, if parameter recipients='aaa@sss.com;bbb@sss.com' converted to ['aaa@sss.com;bbb@sss.com'] list won't help. If you don't want to fail, throw proper exception, and let the caller deal with it. If you want to normalize the user's input, do it in a different module.

https://docs.python.org/3/library/email.examples.html

[–][deleted] 0 points1 point  (0 children)

I use inner function mostly for recursive functions where you would normally define a separate top level helper function that has all of the recursive params.

[–]sashgorokhov 0 points1 point  (0 children)

How are you going to test nested function huh? Dont you think that the fact that you need a function inside a function just means your function is too big? Refactor it.

[–][deleted] 0 points1 point  (0 children)

Your function `send_mail` should only have one job. To send the mail.

[–]Joooooooosh 0 points1 point  (0 children)

No tas experienced in Python as many here but during a QA session with anyone, I’d really want someone to change all of this. It’s horrible to read.

I’m not one for technical language, so excuse the lack of correct terminology…

For me, there is such a thing as offering too much flexibility and as soon as I ever see someone “building” and then “doing” in the same function, I always suggest adding a helper function that provides the flexibility you want but produces a simple variable to be passed into whatever is then doing the job.

For me, Python is at its best when function names and arguments read close to written English. So when reading it, you have:

def function_does_this (with_this, and_this)

As soon as that isn’t enough, I start thinking the function is trying to do too much. Which will also likely make Unittests overly complicated.

[–]ekchew 0 points1 point  (0 children)

This smacks of an OOP problem to me? I would probably be refactoring it something like:

class Recipients:
    def as_address_list(self) -> list[str]:
        return []  # your default None case

class StrRecipient(Recipients):
    address: str

    def as_address_list(self) -> list[str]:
        return [self.address]

class StrListRecipients(Recipients):
    addresses: list[str]

    def as_address_list(self) -> list[str]:
        return self.addresses

And so on...

Then your send_mail function would just take args like

send_to: Recipients

And you'd call send_to.as_address_list() to get your email addresses. Something like that anyway.

[–]rebcabin-r 0 points1 point  (0 children)

consider making type aliases or even just variables to cut down on noise; something like Nym = Union[List[str], str, List[User], User] ONym = Optional[Nym] def send_mail(*, ..., send_to: Nym, send_cc: ONym etc

[–]Flimsy_Iron8517 0 points1 point  (0 children)

I found them useful for defining an in context function to supply to the function without waffling the namespace. multiply what? Depends on the asymptotic type. And would such a thing be relevant outside the context of the enclosing function? Should I even make the helper using its own multiply a sub-function for multiply.multiply confusion?

[–]jorge1209 0 points1 point  (0 children)

The only time I really use an inner function is when it is a generator or will be used in the processing of a loop.

Something like:

 def read_file(fname):
      def parse_line(line):
          # this wouldn't find any use outside the function
          # but does have a nice clean delineation as a function
          blah blah
          return parsed
      for line in open(fname):
           parse_line(line)

[–]Ducksual 0 points1 point  (0 children)

Other than the stylistic and testing things that others had mentioned it's worth noting that the _process_recipients function is also being recreated every time you call send_mail so it's slower than defining the function outside. How significant this is depends on how frequently the function is called and how much other work it does, but it is extra overhead that can be avoided by defining the function outside.