GordonTheTurtle: Please sign your commits following these rules:
https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work
The easiest way to do this is to amend the last commit:
~~~console
$ git clone -b "840-add-exception-utility-method-for-create-host-config" git@github.com:lots0logs/docker-py.git somewhere
$ cd somewhere
$ git rebase -i HEAD~4
editor opens
change each 'pick' to 'edit'
save the file and quit
$ git commit --amend -s --no-edit
$ git rebase --continue # and repeat the amend for each commit
$ git push -f
~~~

Ammending updates the existing PR. You **DO NOT** need to open a new one.

lots0logs: @shin- @dnephin 
aanand: LGTM
dnephin: LGTM, but I think it needs a rebase.

It might be that the gihub "branch protection" is preventing me from merging this because I'm not an owner on this repo.
GordonTheTurtle: Please sign your commits following these rules:
https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work
The easiest way to do this is to amend the last commit:
~~~console
$ git clone -b "840-add-exception-utility-method-for-create-host-config" git@github.com:lots0logs/docker-py.git somewhere
$ cd somewhere
$ git rebase -i HEAD~5
editor opens
change each 'pick' to 'edit'
save the file and quit
$ git commit --amend -s --no-edit
$ git rebase --continue # and repeat the amend for each commit
$ git push -f
~~~

Ammending updates the existing PR. You **DO NOT** need to open a new one.

GordonTheTurtle: Please sign your commits following these rules:
https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work
The easiest way to do this is to amend the last commit:
~~~console
$ git clone -b "840-add-exception-utility-method-for-create-host-config" git@github.com:lots0logs/docker-py.git somewhere
$ cd somewhere
$ git rebase -i HEAD~5
editor opens
change each 'pick' to 'edit'
save the file and quit
$ git commit --amend -s --no-edit
$ git rebase --continue # and repeat the amend for each commit
$ git push -f
~~~

Ammending updates the existing PR. You **DO NOT** need to open a new one.

lots0logs: @dnephin 
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
index 560ee8e2..9c4bb477 100644
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -236,7 +236,7 @@ def convert_port_bindings(port_bindings):
     for k, v in six.iteritems(port_bindings):
         key = str(k)
         if '/' not in key:
-            key = key + '/tcp'
+            key += '/tcp'
         if isinstance(v, list):
             result[key] = [_convert_port_binding(binding) for binding in v]
         else:
@@ -434,7 +434,7 @@ def parse_bytes(s):
         s = 0
     else:
         if s[-2:-1].isalpha() and s[-1].isalpha():
-            if (s[-1] == "b" or s[-1] == "B"):
+            if s[-1] == "b" or s[-1] == "B":
                 s = s[:-1]
         units = BYTE_UNITS
         suffix = s[-1].lower()
@@ -467,16 +467,32 @@ def parse_bytes(s):
     return s
 
 
-def create_host_config(
-    binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=False,
-    links=None, privileged=False, dns=None, dns_search=None, volumes_from=None,
-    network_mode=None, restart_policy=None, cap_add=None, cap_drop=None,
-    devices=None, extra_hosts=None, read_only=None, pid_mode=None,
-    ipc_mode=None, security_opt=None, ulimits=None, log_config=None,
-    mem_limit=None, memswap_limit=None, mem_swappiness=None,
-    cgroup_parent=None, group_add=None, cpu_quota=None, cpu_period=None,
-    oom_kill_disable=False, version=None
-):
+def host_config_type_error(param, param_value, expected):
+    error_msg = 'Invalid type for {0} param: expected {1} but found {2}'
+    return TypeError(error_msg.format(param, expected, type(param_value)))
+
+
+def host_config_version_error(param, version, less_than=True):
+    operator = '<' if less_than else '>'
+    error_msg = '{0} param is not supported in API versions {1} {2}'
+    return errors.InvalidVersion(error_msg.format(param, operator, version))
+
+
+def host_config_value_error(param, param_value):
+    error_msg = 'Invalid value for {0} param: {1}'
+    return ValueError(error_msg.format(param, param_value))
+
+
+def create_host_config(binds=None, port_bindings=None, lxc_conf=None,
+                       publish_all_ports=False, links=None, privileged=False,
+                       dns=None, dns_search=None, volumes_from=None,
+                       network_mode=None, restart_policy=None, cap_add=None,
+                       cap_drop=None, devices=None, extra_hosts=None,
+                       read_only=None, pid_mode=None, ipc_mode=None,
+                       security_opt=None, ulimits=None, log_config=None,
+                       mem_limit=None, memswap_limit=None, mem_swappiness=None,
+                       cgroup_parent=None, group_add=None, cpu_quota=None,
+                       cpu_period=None, oom_kill_disable=False, version=None):
 
     host_config = {}
 
@@ -496,24 +512,21 @@ def create_host_config(
     if memswap_limit is not None:
         if isinstance(memswap_limit, six.string_types):
             memswap_limit = parse_bytes(memswap_limit)
+
         host_config['MemorySwap'] = memswap_limit
 
     if mem_swappiness is not None:
         if version_lt(version, '1.20'):
-            raise errors.InvalidVersion(
-                'mem_swappiness param not supported for API version < 1.20'
-            )
+            raise host_config_version_error('mem_swappiness', '1.20')
         if not isinstance(mem_swappiness, int):
-            raise TypeError(
-                'Invalid type for mem_swappiness param: expected int but'
-                ' found {0}'.format(type(mem_swappiness))
+            raise host_config_type_error(
+                'mem_swappiness', mem_swappiness, 'int'
             )
+
         host_config['MemorySwappiness'] = mem_swappiness
 
     if pid_mode not in (None, 'host'):
-        raise errors.DockerException(
-            'Invalid value for pid param: {0}'.format(pid_mode)
-        )
+        raise host_config_value_error('pid_mode', pid_mode)
     elif pid_mode:
         host_config['PidMode'] = pid_mode
 
@@ -524,10 +537,9 @@ def create_host_config(
         host_config['Privileged'] = privileged
 
     if oom_kill_disable:
-        if version_lt(version, '1.19'):
-            raise errors.InvalidVersion(
-                'oom_kill_disable param not supported for API version < 1.19'
-            )
+        if version_lt(version, '1.20'):
+            raise host_config_version_error('oom_kill_disable', '1.19')
+
         host_config['OomKillDisable'] = oom_kill_disable
 
     if publish_all_ports:
@@ -545,6 +557,11 @@ def create_host_config(
         host_config['NetworkMode'] = 'default'
 
     if restart_policy:
+        if not isinstance(restart_policy, dict):
+            raise host_config_type_error(
+                'restart_policy', restart_policy, 'dict'
+            )
+
         host_config['RestartPolicy'] = restart_policy
 
     if cap_add:
@@ -558,9 +575,8 @@ def create_host_config(
 
     if group_add:
         if version_lt(version, '1.20'):
-            raise errors.InvalidVersion(
-                'group_add param not supported for API version < 1.20'
-            )
+            raise host_config_version_error('group_add', '1.20')
+
         host_config['GroupAdd'] = [six.text_type(grp) for grp in group_add]
 
     if dns is not None:
@@ -568,24 +584,21 @@ def create_host_config(
 
     if security_opt is not None:
         if not isinstance(security_opt, list):
-            raise errors.DockerException(
-                'Invalid type for security_opt param: expected list but found'
-                ' {0}'.format(type(security_opt))
-            )
+            raise host_config_type_error('security_opt', security_opt, 'list')
+
         host_config['SecurityOpt'] = security_opt
 
     if volumes_from is not None:
         if isinstance(volumes_from, six.string_types):
             volumes_from = volumes_from.split(',')
+
         host_config['VolumesFrom'] = volumes_from
 
     if binds is not None:
         host_config['Binds'] = convert_volume_binds(binds)
 
     if port_bindings is not None:
-        host_config['PortBindings'] = convert_port_bindings(
-            port_bindings
-        )
+        host_config['PortBindings'] = convert_port_bindings(port_bindings)
 
     if extra_hosts is not None:
         if isinstance(extra_hosts, dict):
@@ -600,9 +613,7 @@ def create_host_config(
         if isinstance(links, dict):
             links = six.iteritems(links)
 
-        formatted_links = [
-            '{0}:{1}'.format(k, v) for k, v in sorted(links)
-        ]
+        formatted_links = ['{0}:{1}'.format(k, v) for k, v in sorted(links)]
 
         host_config['Links'] = formatted_links
 
@@ -620,10 +631,7 @@ def create_host_config(
 
     if ulimits is not None:
         if not isinstance(ulimits, list):
-            raise errors.DockerException(
-                'Invalid type for ulimits param: expected list but found'
-                ' {0}'.format(type(ulimits))
-            )
+            raise host_config_type_error('ulimits', ulimits, 'list')
         host_config['Ulimits'] = []
         for l in ulimits:
             if not isinstance(l, Ulimit):
@@ -633,35 +641,27 @@ def create_host_config(
     if log_config is not None:
         if not isinstance(log_config, LogConfig):
             if not isinstance(log_config, dict):
-                raise errors.DockerException(
-                    'Invalid type for log_config param: expected LogConfig but'
-                    ' found {0}'.format(type(log_config))
+                raise host_config_type_error(
+                    'log_config', log_config, 'LogConfig'
                 )
             log_config = LogConfig(**log_config)
+
         host_config['LogConfig'] = log_config
 
     if cpu_quota:
         if not isinstance(cpu_quota, int):
-            raise TypeError(
-                'Invalid type for cpu_quota param: expected int but'
-                ' found {0}'.format(type(cpu_quota))
-            )
+            raise host_config_type_error('cpu_quota', cpu_quota, 'int')
         if version_lt(version, '1.19'):
-            raise errors.InvalidVersion(
-                'cpu_quota param not supported for API version < 1.19'
-            )
+            raise host_config_version_error('cpu_quota', '1.19')
+
         host_config['CpuQuota'] = cpu_quota
 
     if cpu_period:
         if not isinstance(cpu_period, int):
-            raise TypeError(
-                'Invalid type for cpu_period param: expected int but'
-                ' found {0}'.format(type(cpu_period))
-            )
+            raise host_config_type_error('cpu_period', cpu_period, 'int')
         if version_lt(version, '1.19'):
-            raise errors.InvalidVersion(
-                'cpu_period param not supported for API version < 1.19'
-            )
+            raise host_config_version_error('cpu_period', '1.19')
+
         host_config['CpuPeriod'] = cpu_period
 
     return host_config

[enhancement] Add utility method to utils.py for raising errors
PR incoming...
diff --git a/tests/integration/container_test.py b/tests/integration/container_test.py
index 03965146..79840a1b 100644
--- a/tests/integration/container_test.py
+++ b/tests/integration/container_test.py
@@ -364,6 +364,13 @@ class CreateContainerTest(helpers.BaseTestCase):
         host_config = inspect['HostConfig']
         self.assertIn('MemorySwappiness', host_config)
 
+    def test_create_host_config_exception_raising(self):
+        self.assertRaises(TypeError,
+                          self.client.create_host_config, mem_swappiness='40')
+
+        self.assertRaises(ValueError,
+                          self.client.create_host_config, pid_mode='40')
+
 
 class VolumeBindTest(helpers.BaseTestCase):
     def setUp(self):
diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py
index 62d64e8a..23fd1913 100644
--- a/tests/unit/api_test.py
+++ b/tests/unit/api_test.py
@@ -314,8 +314,7 @@ class DockerApiTest(DockerClientTest):
         self.assertIn('SecurityOpt', result)
         self.assertEqual(result['SecurityOpt'], security_opt)
         self.assertRaises(
-            docker.errors.DockerException, self.client.create_host_config,
-            security_opt='wrong'
+            TypeError, self.client.create_host_config, security_opt='wrong'
         )
 
 

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov flake8
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
coverage==7.2.7
-e git+https://github.com/docker/docker-py.git@9ebecb5991303d55fe208114a1de422650c4dcb2#egg=docker_py
exceptiongroup==1.2.2
flake8==5.0.4
importlib-metadata==4.2.0
iniconfig==2.0.0
mccabe==0.7.0
packaging==24.0
pluggy==1.2.0
pycodestyle==2.9.1
pyflakes==2.5.0
pytest==7.4.4
pytest-cov==4.1.0
requests==2.5.3
six==1.17.0
tomli==2.0.1
typing_extensions==4.7.1
websocket-client==0.32.0
zipp==3.15.0

name: docker-py
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - mccabe==0.7.0
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - requests==2.5.3
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - websocket-client==0.32.0
      - zipp==3.15.0
prefix: /opt/conda/envs/docker-py

tests/unit/api_test.py::DockerApiTest::test_create_host_config_secopt
tests/unit/api_test.py::DockerApiTest::test_auto_retrieve_server_version tests/unit/api_test.py::DockerApiTest::test_ctor tests/unit/api_test.py::DockerApiTest::test_events tests/unit/api_test.py::DockerApiTest::test_events_with_filters tests/unit/api_test.py::DockerApiTest::test_events_with_since_until tests/unit/api_test.py::DockerApiTest::test_info tests/unit/api_test.py::DockerApiTest::test_remove_link tests/unit/api_test.py::DockerApiTest::test_retrieve_server_version tests/unit/api_test.py::DockerApiTest::test_search tests/unit/api_test.py::DockerApiTest::test_url_compatibility_http tests/unit/api_test.py::DockerApiTest::test_url_compatibility_http_unix_triple_slash tests/unit/api_test.py::DockerApiTest::test_url_compatibility_tcp tests/unit/api_test.py::DockerApiTest::test_url_compatibility_unix tests/unit/api_test.py::DockerApiTest::test_url_compatibility_unix_triple_slash tests/unit/api_test.py::DockerApiTest::test_url_invalid_resource tests/unit/api_test.py::DockerApiTest::test_url_no_resource tests/unit/api_test.py::DockerApiTest::test_url_unversioned_api tests/unit/api_test.py::DockerApiTest::test_url_valid_resource tests/unit/api_test.py::DockerApiTest::test_version tests/unit/api_test.py::DockerApiTest::test_version_no_api_version tests/unit/api_test.py::StreamTest::test_early_stream_response
docker/utils/utils.py
docker/utils/utils.py