Although jonrsharpe provided a great solution, I think it's worth mentioning my approach.
As already mentioned, in dynamic languages ββyou do not need types, namely the interface that the object has (the so-called "duck"). Then your MailRequest object is a great way to group arguments that logically belong together. This, however, is not all that is needed. This will be my approach:
class MailRequest(object): def __init__(self, from_, to, subject, text, bcc=None, cc=None):
And then the send_email function will look like this:
def send_email(self, mailReq): """ :type mailReq: MailRequest """ from_ = mailReq.from_ to = mailReq.to subject= mailReq.subject text = mailReq.text bcc = mailReq.bcc cc = mailReq.cc
Note that you are simply documenting the mailReq argument, indicating that any object passed in should provide the MailRequest interface (at least in part). This way you delegate the documentation of the arguments to the MailRequest class.
I prefer this approach to magic **kwargs because the arguments are explicitly passed at some point to a tough signature that somehow serves as documentation. The disadvantage is verbosity.
EDIT
If you are afraid of arguments MailRequest in the MailRequest constructor, the solution should make the same level even deeper: group again. For example, you can group the parameters into your own class:
class MailRequestOpts(object): def __init__(self, bbc=None, cc=None, colour=None, lights='blue', blink=True):
And then MailRequestClass will look like this:
class MailRequest(object): def __init__(self, from_, to, subject, text, options=None): """ :type options: MailRequestOpts """ if options is None: options = MailRequestOpts()
In the end, if you need 50 arguments for a process, you cannot skip them in one or more distributed functions at any time. How many times you group them is up to you and where you find the balance.