The surest way of identifying whether the user has provided a value is to leave the default at the default None. There's nothing the user can say that will produce the same thing. Then after parsing all you need to do is:
if args.myval is None:
# no user input, set my own special default
else:
# user value
If you set the default=argparse.SUPPRESS, the attribute will be omitted from the args namespace. But testing for that case it a bit more awkward.
args.myval
will raise an error, which you could catch. Or you could use hasattr(args, 'myval'). Or handle the missing key invars(args)`.
Internally parse_args maintains a list (set actually) of Actions that it has 'seen'. That's used to test for required Actions. But that isn't available to users. If it were available, sophisticated users could perform a wider variety of co-occurrence tests.
In [28]: import argparse
In [29]: parser = argparse.ArgumentParser()
In [30]: a1 = parser.add_argument('-f','--foo')
Test without a value:
In [31]: args = parser.parse_args([])
In [32]: args
Out[32]: Namespace(foo=None) # foo is present
In [33]: args.foo is None
Out[33]: True
Change the default to SUPPRESS:
In [35]: a1.default = argparse.SUPPRESS
In [36]: args = parser.parse_args([])
In [37]: args
Out[37]: Namespace() # foo is missing
In [38]: args.foo
AttributeError: 'Namespace' object has no attribute 'foo'
We could put args.foo in a try: except AttributeError block.
In [39]: hasattr(args, 'foo')
Out[39]: False
Or dictionary testing for missing value
In [46]: vars(args)
Out[46]: {}
In [47]: vars(args).get('foo') is None
Out[47]: True
In [48]: 'foo' in vars(args)
Out[48]: False
A default is most useful when it provides a valid, useful value, and it saves you from extra testing after parsing.