Repository: omni-us/jsonargparse

Issue:
Help docstring for Callable does not work as intended
<!-- If you like this project, please ⭐ star it https://github.com/omni-us/jsonargparse/ -->

When using a `Callable` argument that returns one class out of a union of classes, the help string does not show the docstring of the classes. This bug has been mentioned [here](https://github.com/omni-us/jsonargparse/issues/565#issuecomment-2328233859)  before. This particular use case is as in the issue mentioned above, we want to select and parameterize a pytorch optimizer via the command line and display help strings for the choice the user makes.

<!-- A clear and concise description of the bug. -->

### To reproduce

```python
from jsonargparse import ArgumentParser
from typing import Callable, Iterable
from torch.optim import SGD, Adam, AdamW

parser = ArgumentParser(exit_on_error=False)
# Here add to the parser only argument(s) relevant to the problem
parser.add_argument("--optimizer", type=Callable[[Iterable], SGD | Adam | AdamW])

# Preferable that the command line arguments are given to the parse_args call
result = parser.parse_args(["--optimizer.help=SGD"])

# If the problem is in the parsed result, print it to stdout
print(parser.dump(result))
```
Gives the following error:
```
argparse.ArgumentError: Parser key "optimizer": Type typing.Callable[[typing.Iterable], torch.optim.sgd.SGD | torch.optim.adam.Adam | torch.optim.adamw.AdamW] expects a function or a callable class: Dict must include a class_path and optionally init_args, but got NestedArg(key='help', val='SGD'). Got value: Namespace(help='SGD')
```

### Expected behavior
 To print the docstring and parameters descriptions provided by `torch.optim.SGD` or any other selected class.
<!-- Describe how the behavior or output of the reproduction snippet should be. -->

### Environment

<!-- Fill in the list below. -->

- jsonargparse version: 4.32.1
- Python version: 3.10
- How jsonargparse was installed: pip
- OS: macOS 14.4


Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=567) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=567&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=567&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=567)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=567) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=567&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=567&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=567)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
Attention: Patch coverage is `95.83333%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 99.92%. Comparing base [(`e5ff8bc`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/e5ff8bcf25181c51b4082d8a46778308ccac2df0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`08d6ddb`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/08d6ddb269f290d4c3dbb6a7ea9ea5c26d1029bf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

| [Files with missing lines](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Patch % | Lines |
|---|---|---|
| [jsonargparse/\_actions.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567?src=pr&el=tree&filepath=jsonargparse%2F_actions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19hY3Rpb25zLnB5) | 86.66% | [2 Missing :warning: ](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main     #567      +/-   ##
===========================================
- Coverage   100.00%   99.92%   -0.08%     
===========================================
  Files           22       22              
  Lines         6445     6451       +6     
===========================================
+ Hits          6445     6446       +1     
- Misses           0        5       +5     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.67% <95.83%> (-0.07%)` | :arrow_down: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.61% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.67% <54.16%> (-0.07%)` | :arrow_down: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.45% <54.16%> (-0.07%)` | :arrow_down: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.63% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.65% <95.83%> (-0.07%)` | :arrow_down: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.61% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.63% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.79% <95.83%> (-0.07%)` | :arrow_down: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.61% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.63% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.11% <95.83%> (-0.07%)` | :arrow_down: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.12% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.17% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.30% <95.83%> (-0.07%)` | :arrow_down: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.28% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.31% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.19% <95.83%> (-0.07%)` | :arrow_down: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.20% <95.83%> (-0.08%)` | :arrow_down: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.22% <95.83%> (-0.08%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/567?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=567) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=567&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=567&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=567&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=567)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 16f3d81..b9ab9ff 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in
 paths are considered internals and can change in minor and patch releases.
 
 
+v4.32.2 (2024-09-??)
+--------------------
+
+Fixed
+^^^^^
+- Callable type with subclass return not showing the ``--*.help`` option (`#567
+  <https://github.com/omni-us/jsonargparse/pull/567>`__).
+
+
 v4.32.1 (2024-08-23)
 --------------------
 
diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py
index e2bb047..e2bcada 100644
--- a/jsonargparse/_actions.py
+++ b/jsonargparse/_actions.py
@@ -15,13 +15,11 @@ from ._namespace import Namespace, NSKeyError, split_key, split_key_root
 from ._optionals import get_config_read_mode
 from ._type_checking import ArgumentParser
 from ._util import (
-    NoneType,
     Path,
     argument_error,
     change_to_path_dir,
     default_config_option_help,
     get_import_path,
-    get_typehint_origin,
     import_object,
     indent_text,
     iter_to_set_str,
@@ -343,22 +341,18 @@ class _ActionConfigLoad(Action):
 class _ActionHelpClassPath(Action):
     sub_add_kwargs: Dict[str, Any] = {}
 
-    def __init__(self, baseclass=None, **kwargs):
-        if baseclass is not None:
-            if get_typehint_origin(baseclass) == Union:
-                baseclasses = [c for c in baseclass.__args__ if c is not NoneType]
-                if len(baseclasses) == 1:
-                    baseclass = baseclasses[0]
-            self._baseclass = baseclass
+    def __init__(self, typehint=None, **kwargs):
+        if typehint is not None:
+            self._typehint = typehint
         else:
-            self._baseclass = kwargs.pop("_baseclass")
+            self._typehint = kwargs.pop("_typehint")
             self.update_init_kwargs(kwargs)
             super().__init__(**kwargs)
 
     def update_init_kwargs(self, kwargs):
-        from ._typehints import get_subclasses_from_type
+        from ._typehints import get_subclass_names
 
-        self._basename = iter_to_set_str(get_subclasses_from_type(self._baseclass))
+        self._basename = iter_to_set_str(get_subclass_names(self._typehint, callable_return=True))
         kwargs.update(
             {
                 "metavar": "CLASS_PATH_OR_NAME",
@@ -369,26 +363,32 @@ class _ActionHelpClassPath(Action):
 
     def __call__(self, *args, **kwargs):
         if len(args) == 0:
-            kwargs["_baseclass"] = self._baseclass
+            kwargs["_typehint"] = self._typehint
             return type(self)(**kwargs)
-        dest = re.sub("\\.help$", "", self.dest)
-        return self.print_help(args, self._baseclass, dest)
-
-    def print_help(self, call_args, baseclass, dest):
-        from ._typehints import resolve_class_path_by_name
+        return self.print_help(args)
+
+    def print_help(self, call_args):
+        from ._typehints import (
+            ActionTypeHint,
+            get_optional_arg,
+            get_subclass_types,
+            get_unaliased_type,
+            resolve_class_path_by_name,
+        )
 
         parser, _, value, option_string = call_args
         try:
-            val_class = import_object(resolve_class_path_by_name(baseclass, value))
+            typehint = get_unaliased_type(get_optional_arg(self._typehint))
+            baseclasses = get_subclass_types(typehint, callable_return=True)
+            val_class = import_object(resolve_class_path_by_name(typehint, value))
         except Exception as ex:
             raise TypeError(f"{option_string}: {ex}") from ex
-        if get_typehint_origin(self._baseclass) == Union:
-            baseclasses = self._baseclass.__args__
-        else:
-            baseclasses = [baseclass]
         if not any(is_subclass(val_class, b) for b in baseclasses):
             raise TypeError(f'{option_string}: Class "{value}" is not a subclass of {self._basename}')
+        dest = re.sub("\\.help$", "", self.dest)
         subparser = type(parser)(description=f"Help for {option_string}={get_import_path(val_class)}")
+        if ActionTypeHint.is_callable_typehint(typehint) and hasattr(typehint, "__args__"):
+            self.sub_add_kwargs["skip"] = {max(0, len(typehint.__args__) - 1)}
         subparser.add_class_arguments(val_class, dest, **self.sub_add_kwargs)
         remove_actions(subparser, (_HelpAction, _ActionPrintConfig, _ActionConfigLoad))
         args = self.get_args_after_opt(parser.args)
diff --git a/jsonargparse/_completions.py b/jsonargparse/_completions.py
index 15e6b36..000ce2e 100644
--- a/jsonargparse/_completions.py
+++ b/jsonargparse/_completions.py
@@ -170,7 +170,7 @@ def shtab_prepare_action(action, parser) -> None:
             add_bash_typehint_completion(parser, action, message, choices)
             choices = None
     elif isinstance(action, _ActionHelpClassPath):
-        choices = get_help_class_choices(action._baseclass)
+        choices = get_help_class_choices(action._typehint)
     if choices:
         action.choices = choices
 
diff --git a/jsonargparse/_parameter_resolvers.py b/jsonargparse/_parameter_resolvers.py
index 29712ea..8279fc7 100644
--- a/jsonargparse/_parameter_resolvers.py
+++ b/jsonargparse/_parameter_resolvers.py
@@ -79,10 +79,10 @@ class ConditionalDefault(UnknownDefault):
 
 
 def get_parameter_origins(component, parent) -> Optional[str]:
-    from ._typehints import get_subclasses_from_type, sequence_origin_types
+    from ._typehints import get_subclass_types, sequence_origin_types
 
     if get_typehint_origin(component) in sequence_origin_types:
-        component = get_subclasses_from_type(component, names=False)
+        component = get_subclass_types(component, also_lists=True)
     if isinstance(component, tuple):
         assert parent is None or len(component) == len(parent)
         return iter_to_set_str(get_parameter_origins(c, parent[n] if parent else None) for n, c in enumerate(component))
@@ -357,12 +357,12 @@ def is_param_subclass_instance_default(param: ParamData) -> bool:
     from ._typehints import ActionTypeHint, get_optional_arg, get_subclass_types
 
     annotation = get_optional_arg(param.annotation)
-    class_types = get_subclass_types(annotation)
+    class_types = get_subclass_types(annotation, callable_return=True)
     return bool(
         (class_types and isinstance(param.default, class_types))
         or (
             is_lambda(param.default)
-            and ActionTypeHint.is_callable_typehint(annotation, all_subtypes=False)
+            and ActionTypeHint.is_callable_typehint(annotation)
             and getattr(annotation, "__args__", None)
             and ActionTypeHint.is_subclass_typehint(annotation.__args__[-1], all_subtypes=False)
         )
@@ -684,7 +684,7 @@ class ParametersVisitor(LoggerProperty, ast.NodeVisitor):
                     node = default_node.body
                     num_positionals = len(param.annotation.__args__) - 1
                 class_type = self.get_call_class_type(node)
-                subclass_types = get_subclass_types(param.annotation)
+                subclass_types = get_subclass_types(param.annotation, callable_return=True)
                 if not (class_type and subclass_types and is_subclass(class_type, subclass_types)):
                     continue
                 subclass_spec: dict = dict(class_path=get_import_path(class_type), init_args=dict())
diff --git a/jsonargparse/_signatures.py b/jsonargparse/_signatures.py
index c66871d..639adc6 100644
--- a/jsonargparse/_signatures.py
+++ b/jsonargparse/_signatures.py
@@ -27,7 +27,7 @@ from ._typehints import (
     ActionTypeHint,
     LazyInitBaseClass,
     callable_instances,
-    get_subclasses_from_type,
+    get_subclass_names,
     is_optional,
 )
 from ._util import NoneType, get_private_kwargs, iter_to_set_str
@@ -541,7 +541,7 @@ class SignatureArguments(LoggerProperty):
         if skip is not None:
             skip = {f"{nested_key}.init_args." + s for s in skip}
         param = ParamData(name=nested_key, annotation=Union[baseclass], component=baseclass)
-        str_baseclass = iter_to_set_str(get_subclasses_from_type(param.annotation))
+        str_baseclass = iter_to_set_str(get_subclass_names(param.annotation))
         kwargs.update(
             {
                 "metavar": metavar,
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index aeaaad2..43dff94 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -250,9 +250,11 @@ class ActionTypeHint(Action):
         typehint = kwargs.pop("type")
         if args[0].startswith("--") and ActionTypeHint.supports_append(typehint):
             args = tuple(list(args) + [args[0] + "+"])
-        if ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False):
+        if ActionTypeHint.is_subclass_typehint(
+            typehint, all_subtypes=False
+        ) or ActionTypeHint.is_return_subclass_typehint(typehint):
             help_option = f"--{args[0]}.help" if args[0][0] != "-" else f"{args[0]}.help"
-            help_action = container.add_argument(help_option, action=_ActionHelpClassPath(baseclass=typehint))
+            help_action = container.add_argument(help_option, action=_ActionHelpClassPath(typehint=typehint))
             if sub_add_kwargs:
                 help_action.sub_add_kwargs = sub_add_kwargs
         kwargs["action"] = ActionTypeHint(typehint=typehint, enable_path=enable_path, logger=logger)
@@ -303,15 +305,7 @@ class ActionTypeHint(Action):
             test = all if all_subtypes else any
             k = {"also_lists": also_lists}
             return test(ActionTypeHint.is_subclass_typehint(s, **k) for s in subtypes)
-        return (
-            inspect.isclass(typehint)
-            and typehint not in leaf_or_root_types
-            and not get_registered_type(typehint)
-            and not is_pydantic_type(typehint)
-            and not is_dataclass_like(typehint)
-            and typehint_origin is None
-            and not is_subclass(typehint, (Path, Enum))
-        )
+        return is_single_subclass_typehint(typehint, typehint_origin)
 
     @staticmethod
     def is_return_subclass_typehint(typehint):
@@ -336,14 +330,10 @@ class ActionTypeHint(Action):
         return False
 
     @staticmethod
-    def is_callable_typehint(typehint, all_subtypes=True):
-        typehint = get_unaliased_type(typehint)
-        typehint_origin = get_typehint_origin(typehint)
-        if typehint_origin == Union:
-            subtypes = [a for a in typehint.__args__ if a != NoneType]
-            test = all if all_subtypes else any
-            return test(ActionTypeHint.is_callable_typehint(s) for s in subtypes)
-        return typehint_origin in callable_origin_types or typehint in callable_origin_types
+    def is_callable_typehint(typehint):
+        typehint = typehint_from_action(typehint)
+        typehint_origin = get_typehint_origin(get_optional_arg(get_unaliased_type(typehint)))
+        return typehint_origin in callable_origin_types
 
     def is_init_arg_mapping_typehint(self, key, cfg):
         result = False
@@ -635,10 +625,13 @@ class ActionTypeHint(Action):
 
     def extra_help(self):
         extra = ""
-        if self.is_subclass_typehint(self, all_subtypes=False) or get_typehint_origin(
-            self._typehint
+        typehint = get_optional_arg(self._typehint)
+        if self.is_subclass_typehint(typehint, all_subtypes=False) or get_typehint_origin(
+            typehint
         ) in callable_origin_types.union({Type, type}):
-            class_paths = get_all_subclass_paths(self._typehint)
+            if self.is_callable_typehint(typehint) and getattr(typehint, "__args__", None):
+                typehint = get_callable_return_type(get_optional_arg(typehint))
+            class_paths = get_all_subclass_paths(typehint)
             if class_paths:
                 extra = ", known subclasses: " + ", ".join(class_paths)
         return extra
@@ -680,22 +673,6 @@ def is_pathlike(typehint) -> bool:
     return is_subclass(typehint, os.PathLike)
 
 
-def get_subclasses_from_type(typehint, names=True, subclasses=None) -> tuple:
-    if subclasses is None:
-        subclasses = []
-    origin = get_typehint_origin(typehint)
-    if origin == Union or origin in sequence_origin_types:
-        for subtype in typehint.__args__:
-            get_subclasses_from_type(subtype, names, subclasses)
-    elif ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False):
-        if names:
-            if typehint.__name__ not in subclasses:
-                subclasses.append(typehint.__name__)
-        elif typehint not in subclasses:
-            subclasses.append(typehint)
-    return tuple(subclasses)
-
-
 def raise_unexpected_value(message: str, val: Any = inspect._empty, exception: Optional[Exception] = None) -> NoReturn:
     if val is not inspect._empty:
         message += f". Got value: {val}"
@@ -1143,21 +1120,44 @@ def get_callable_return_type(typehint):
     return return_type
 
 
-def get_subclass_types(typehint, callable_return=True):
-    subclass_types = None
-    if (
-        callable_return
-        and ActionTypeHint.is_callable_typehint(typehint, all_subtypes=False)
-        and getattr(typehint, "__args__", None)
-    ):
-        typehint = get_optional_arg(typehint)
-        typehint = typehint.__args__[-1]
-    if ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False):
-        if get_typehint_origin(typehint) == Union:
-            subclass_types = tuple(t for t in typehint.__args__ if ActionTypeHint.is_subclass_typehint(t))
-        else:
-            subclass_types = (typehint,)
-    return subclass_types
+def is_single_subclass_typehint(typehint, typehint_origin):
+    return (
+        inspect.isclass(typehint)
+        and typehint not in leaf_or_root_types
+        and not get_registered_type(typehint)
+        and not is_pydantic_type(typehint)
+        and not is_dataclass_like(typehint)
+        and typehint_origin is None
+        and not is_subclass(typehint, (Path, Enum))
+    )
+
+
+def yield_subclass_types(typehint, also_lists=False, callable_return=False):
+    typehint = typehint_from_action(typehint)
+    if typehint is None:
+        return
+    typehint = get_unaliased_type(get_optional_arg(get_unaliased_type(typehint)))
+    typehint_origin = get_typehint_origin(typehint)
+    if callable_return and typehint_origin in callable_origin_types:
+        return_type = get_callable_return_type(typehint)
+        if return_type:
+            k = {"also_lists": also_lists, "callable_return": callable_return}
+            yield from yield_subclass_types(return_type, **k)
+    elif typehint_origin == Union or (also_lists and typehint_origin in sequence_origin_types):
+        k = {"also_lists": also_lists, "callable_return": callable_return}
+        for subtype in typehint.__args__:
+            yield from yield_subclass_types(subtype, **k)
+    if is_single_subclass_typehint(typehint, typehint_origin):
+        yield typehint
+
+
+def get_subclass_types(typehint, also_lists=False, callable_return=False):
+    types = tuple(yield_subclass_types(typehint, also_lists=also_lists, callable_return=callable_return))
+    return types or None
+
+
+def get_subclass_names(typehint, callable_return=False):
+    return tuple(t.__name__ for t in yield_subclass_types(typehint, callable_return=callable_return))
 
 
 def adapt_partial_callable_class(callable_type, subclass_spec):
@@ -1165,7 +1165,7 @@ def adapt_partial_callable_class(callable_type, subclass_spec):
     num_partial_args = 0
     return_type = get_callable_return_type(callable_type)
     if return_type:
-        subclass_types = get_subclass_types(return_type, callable_return=False)
+        subclass_types = get_subclass_types(return_type)
         class_type = import_object(resolve_class_path_by_name(return_type, subclass_spec.class_path))
         if subclass_types and is_subclass(class_type, subclass_types):
             subclass_spec = subclass_spec.clone()


Test patch:
diff --git a/jsonargparse_tests/test_shtab.py b/jsonargparse_tests/test_shtab.py
index 9802357..f8d8734 100644
--- a/jsonargparse_tests/test_shtab.py
+++ b/jsonargparse_tests/test_shtab.py
@@ -296,7 +296,7 @@ def test_bash_nested_subclasses(parser, subtests):
 def test_bash_callable_return_class(parser, subtests):
     parser.add_argument("--cls", type=Callable[[int], Base])
     shtab_script = get_shtab_script(parser, "bash")
-    assert "_option_strings=('-h' '--help' '--cls' '--cls.p2' '--cls.p3')" in shtab_script
+    assert "_option_strings=('-h' '--help' '--cls.help' '--cls' '--cls.p2' '--cls.p3')" in shtab_script
     assert "--cls.p1" not in shtab_script
     classes = f"{__name__}.Base {__name__}.SubA {__name__}.SubB".split()
     assert_bash_typehint_completions(
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 4183bf6..c391a76 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -38,6 +38,7 @@ from jsonargparse._typehints import (
     ActionTypeHint,
     Literal,
     get_all_subclass_paths,
+    get_subclass_types,
     is_optional,
 )
 from jsonargparse._util import get_import_path
@@ -753,6 +754,24 @@ class Adam(Optimizer):
         super().__init__(*args, **kwargs)
 
 
+@pytest.mark.parametrize(
+    ["typehint", "expected"],
+    [
+        (None, None),
+        (Union[int, float], None),
+        (Optimizer, (Optimizer,)),
+        (Union[SGD, Adam, str], (SGD, Adam)),
+        (Optional[Union[SGD, Adam]], (SGD, Adam)),
+        (Callable[[int], Optimizer], (Optimizer,)),
+        (Callable[[int], Union[Adam, SGD]], (Adam, SGD)),
+        (Optional[Callable[[int], Union[Adam, SGD]]], (Adam, SGD)),
+        (Union[Optimizer, Callable[[int], Union[Adam, SGD]]], (Optimizer, Adam, SGD)),
+    ],
+)
+def test_get_subclass_types(typehint, expected):
+    assert expected == get_subclass_types(typehint, callable_return=True)
+
+
 def test_callable_args_return_type_class(parser, subtests):
     parser.add_argument("--optimizer", type=Callable[[List[float]], Optimizer], default=SGD)
 
@@ -789,8 +808,24 @@ def test_callable_args_return_type_class(parser, subtests):
 
     with subtests.test("help"):
         help_str = get_parser_help(parser)
+        assert "--optimizer.help" in help_str
+        assert "Show the help for the given subclass of Optimizer" in help_str
         for name in ["Optimizer", "SGD", "Adam"]:
             assert f"{__name__}.{name}" in help_str
+        help_str = get_parse_args_stdout(parser, ["--optimizer.help=Adam"])
+        assert f"Help for --optimizer.help={__name__}.Adam" in help_str
+        assert "--optimizer.lr" in help_str
+        assert "--optimizer.params" not in help_str
+
+
+def test_optional_callable_return_type_help(parser):
+    parser.add_argument("--optimizer", type=Optional[Callable[[List[float]], Optimizer]])
+    help_str = get_parser_help(parser)
+    assert "--optimizer.help" in help_str
+    assert f"known subclasses: {__name__}.Optimizer," in help_str
+    help_str = get_parse_args_stdout(parser, ["--optimizer.help=Adam"])
+    assert f"Help for --optimizer.help={__name__}.Adam" in help_str
+    assert "--optimizer.lr" in help_str
 
 
 def test_callable_return_type_class_implicit_class_path(parser):
@@ -837,6 +872,7 @@ def test_callable_multiple_args_return_type_class(parser, subtests):
 
     with subtests.test("help"):
         help_str = get_parser_help(parser)
+        assert "Show the help for the given subclass of Optimizer" in help_str
         for name in ["Optimizer", "SGD", "Adam"]:
             assert f"{__name__}.{name}" in help_str
 
@@ -895,6 +931,7 @@ def test_callable_args_return_type_union_of_classes(parser, subtests):
 
     with subtests.test("help"):
         help_str = get_parser_help(parser)
+        assert "Show the help for the given subclass of {StepLR,ReduceLROnPlateau}" in help_str
         for name in ["StepLR", "ReduceLROnPlateau"]:
             assert f"{__name__}.{name}" in help_str
 
@@ -935,6 +972,10 @@ def test_optional_callable_args_return_type_class(parser, subtests):
         cfg = parser.parse_args(["--scheduler=null"])
         assert cfg.scheduler is None
 
+    with subtests.test("help"):
+        help_str = get_parser_help(parser)
+        assert "Show the help for the given subclass of StepLR" in help_str
+
 
 class CallableSubconfig:
     def __init__(self, o: Callable[[int], Optimizer]):