diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index 09d8228f986e47..e1f29ae051e2fa 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -784,6 +784,11 @@ process and user.
    :func:`socket.gethostname`  or even
    ``socket.gethostbyaddr(socket.gethostname())``.
 
+   On macOS, iOS and Android, this returns the *kernel* name and version (i.e.,
+   ``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()`
+   can be used to get the user-facing operating system name and version on iOS and
+   Android.
+
    .. availability:: Unix.
 
    .. versionchanged:: 3.3
diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst
index 6af9168d15749f..069dab791dcbe5 100644
--- a/Doc/library/platform.rst
+++ b/Doc/library/platform.rst
@@ -148,6 +148,9 @@ Cross Platform
    Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``,
    ``'Windows'``. An empty string is returned if the value cannot be determined.
 
+   On iOS and Android, this returns the user-facing OS name (i.e, ``'iOS``,
+   ``'iPadOS'`` or ``'Android'``). To obtain the kernel name (``'Darwin'`` or
+   ``'Linux'``), use :func:`os.uname()`.
 
 .. function:: system_alias(system, release, version)
 
@@ -161,6 +164,8 @@ Cross Platform
    Returns the system's release version, e.g. ``'#3 on degas'``. An empty string is
    returned if the value cannot be determined.
 
+   On iOS and Android, this is the user-facing OS version. To obtain the
+   Darwin or Linux kernel version, use :func:`os.uname()`.
 
 .. function:: uname()
 
@@ -238,7 +243,6 @@ Windows Platform
 macOS Platform
 --------------
 
-
 .. function:: mac_ver(release='', versioninfo=('','',''), machine='')
 
    Get macOS version information and return it as tuple ``(release, versioninfo,
@@ -248,6 +252,24 @@ macOS Platform
    Entries which cannot be determined are set to ``''``.  All tuple entries are
    strings.
 
+iOS Platform
+------------
+
+.. function:: ios_ver(system='', release='', model='', is_simulator=False)
+
+   Get iOS version information and return it as a
+   :func:`~collections.namedtuple` with the following attributes:
+
+   * ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``.
+   * ``release`` is the iOS version number as a string (e.g., ``'17.2'``).
+   * ``model`` is the device model identifier; this will be a string like
+     ``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator.
+   * ``is_simulator`` is a boolean describing if the app is running on a
+     simulator or a physical device.
+
+   Entries which cannot be determined are set to the defaults given as
+   parameters.
+
 
 Unix Platforms
 --------------
diff --git a/Doc/library/webbrowser.rst b/Doc/library/webbrowser.rst
index 4667b81e38ada2..c1c4619d9df776 100644
--- a/Doc/library/webbrowser.rst
+++ b/Doc/library/webbrowser.rst
@@ -33,6 +33,13 @@ allow the remote browser to maintain its own windows on the display.  If remote
 browsers are not available on Unix, the controlling process will launch a new
 browser and wait.
 
+On iOS, the :envvar:`BROWSER` environment variable, as well as any arguments
+controlling autoraise, browser preference, and new tab/window creation will be
+ignored. Web pages will *always* be opened in the user's preferred browser, in
+a new tab, with the browser being brought to the foreground. The use of the
+:mod:`webbrowser` module on iOS requires the :mod:`ctypes` module. If
+:mod:`ctypes` isn't available, calls to :func:`.open` will fail.
+
 The script :program:`webbrowser` can be used as a command-line interface for the
 module. It accepts a URL as the argument. It accepts the following optional
 parameters: ``-n`` opens the URL in a new browser window, if possible;
@@ -147,6 +154,8 @@ for the controller classes, all defined in this module.
 +------------------------+-----------------------------------------+-------+
 | ``'chromium-browser'`` | ``Chromium('chromium-browser')``        |       |
 +------------------------+-----------------------------------------+-------+
+| ``'iosbrowser'``       | ``IOSBrowser``                          | \(4)  |
++------------------------+-----------------------------------------+-------+
 
 Notes:
 
@@ -161,7 +170,10 @@ Notes:
    Only on Windows platforms.
 
 (3)
-   Only on macOS platform.
+   Only on macOS.
+
+(4)
+   Only on iOS.
 
 .. versionadded:: 3.2
    A new :class:`!MacOSXOSAScript` class has been added
@@ -176,6 +188,9 @@ Notes:
    Removed browsers include Grail, Mosaic, Netscape, Galeon,
    Skipstone, Iceape, and Firefox versions 35 and below.
 
+.. versionchanged:: 3.13
+   Support for iOS has been added.
+
 Here are some simple examples::
 
    url = 'https://docs.python.org/'
diff --git a/Lib/_ios_support.py b/Lib/_ios_support.py
new file mode 100644
index 00000000000000..db3fe23e45bca0
--- /dev/null
+++ b/Lib/_ios_support.py
@@ -0,0 +1,71 @@
+import sys
+try:
+    from ctypes import cdll, c_void_p, c_char_p, util
+except ImportError:
+    # ctypes is an optional module. If it's not present, we're limited in what
+    # we can tell about the system, but we don't want to prevent the module
+    # from working.
+    print("ctypes isn't available; iOS system calls will not be available")
+    objc = None
+else:
+    # ctypes is available. Load the ObjC library, and wrap the objc_getClass,
+    # sel_registerName methods
+    lib = util.find_library("objc")
+    if lib is None:
+        # Failed to load the objc library
+        raise RuntimeError("ObjC runtime library couldn't be loaded")
+
+    objc = cdll.LoadLibrary(lib)
+    objc.objc_getClass.restype = c_void_p
+    objc.objc_getClass.argtypes = [c_char_p]
+    objc.sel_registerName.restype = c_void_p
+    objc.sel_registerName.argtypes = [c_char_p]
+
+
+def get_platform_ios():
+    # Determine if this is a simulator using the multiarch value
+    is_simulator = sys.implementation._multiarch.endswith("simulator")
+
+    # We can't use ctypes; abort
+    if not objc:
+        return None
+
+    # Most of the methods return ObjC objects
+    objc.objc_msgSend.restype = c_void_p
+    # All the methods used have no arguments.
+    objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
+
+    # Equivalent of:
+    #   device = [UIDevice currentDevice]
+    UIDevice = objc.objc_getClass(b"UIDevice")
+    SEL_currentDevice = objc.sel_registerName(b"currentDevice")
+    device = objc.objc_msgSend(UIDevice, SEL_currentDevice)
+
+    # Equivalent of:
+    #   device_systemVersion = [device systemVersion]
+    SEL_systemVersion = objc.sel_registerName(b"systemVersion")
+    device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)
+
+    # Equivalent of:
+    #   device_systemName = [device systemName]
+    SEL_systemName = objc.sel_registerName(b"systemName")
+    device_systemName = objc.objc_msgSend(device, SEL_systemName)
+
+    # Equivalent of:
+    #   device_model = [device model]
+    SEL_model = objc.sel_registerName(b"model")
+    device_model = objc.objc_msgSend(device, SEL_model)
+
+    # UTF8String returns a const char*;
+    SEL_UTF8String = objc.sel_registerName(b"UTF8String")
+    objc.objc_msgSend.restype = c_char_p
+
+    # Equivalent of:
+    #   system = [device_systemName UTF8String]
+    #   release = [device_systemVersion UTF8String]
+    #   model = [device_model UTF8String]
+    system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
+    release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
+    model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
+
+    return system, release, model, is_simulator
diff --git a/Lib/platform.py b/Lib/platform.py
index df1d987036455f..dbcb636df64981 100755
--- a/Lib/platform.py
+++ b/Lib/platform.py
@@ -496,6 +496,30 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
     # If that also doesn't work return the default values
     return release, versioninfo, machine
 
+
+# A namedtuple for iOS version information.
+IOSVersionInfo = collections.namedtuple(
+    "IOSVersionInfo",
+    ["system", "release", "model", "is_simulator"]
+)
+
+
+def ios_ver(system="", release="", model="", is_simulator=False):
+    """Get iOS version information, and return it as a namedtuple:
+        (system, release, model, is_simulator).
+
+    If values can't be determined, they are set to values provided as
+    parameters.
+    """
+    if sys.platform == "ios":
+        import _ios_support
+        result = _ios_support.get_platform_ios()
+        if result is not None:
+            return IOSVersionInfo(*result)
+
+    return IOSVersionInfo(system, release, model, is_simulator)
+
+
 def _java_getprop(name, default):
     """This private helper is deprecated in 3.13 and will be removed in 3.15"""
     from java.lang import System
@@ -654,7 +678,7 @@ def _platform(*args):
         if cleaned == platform:
             break
         platform = cleaned
-    while platform[-1] == '-':
+    while platform and platform[-1] == '-':
         platform = platform[:-1]
 
     return platform
@@ -695,7 +719,7 @@ def _syscmd_file(target, default=''):
         default in case the command should fail.
 
     """
-    if sys.platform in ('dos', 'win32', 'win16'):
+    if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}:
         # XXX Others too ?
         return default
 
@@ -859,6 +883,14 @@ def get_OpenVMS():
             csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
             return 'Alpha' if cpu_number >= 128 else 'VAX'
 
+    # On the iOS simulator, os.uname returns the architecture as uname.machine.
+    # On device it returns the model name for some reason; but there's only one
+    # CPU architecture for iOS devices, so we know the right answer.
+    def get_ios():
+        if sys.implementation._multiarch.endswith("simulator"):
+            return os.uname().machine
+        return 'arm64'
+
     def from_subprocess():
         """
         Fall back to `uname -p`
@@ -1018,6 +1050,10 @@ def uname():
         system = 'Android'
         release = android_ver().release
 
+    # Normalize responses on iOS
+    if sys.platform == 'ios':
+        system, release, _, _ = ios_ver()
+
     vals = system, node, release, version, machine
     # Replace 'unknown' values with the more portable ''
     _uname_cache = uname_result(*map(_unknown_as_blank, vals))
@@ -1297,11 +1333,14 @@ def platform(aliased=False, terse=False):
         system, release, version = system_alias(system, release, version)
 
     if system == 'Darwin':
-        # macOS (darwin kernel)
-        macos_release = mac_ver()[0]
-        if macos_release:
-            system = 'macOS'
-            release = macos_release
+        # macOS and iOS both report as a "Darwin" kernel
+        if sys.platform == "ios":
+            system, release, _, _ = ios_ver()
+        else:
+            macos_release = mac_ver()[0]
+            if macos_release:
+                system = 'macOS'
+                release = macos_release
 
     if system == 'Windows':
         # MS platforms
diff --git a/Lib/site.py b/Lib/site.py
index 2aee63e24ca52b..162bbec4f8f41b 100644
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -280,8 +280,8 @@ def _getuserbase():
     if env_base:
         return env_base
 
-    # Emscripten, VxWorks, and WASI have no home directories
-    if sys.platform in {"emscripten", "vxworks", "wasi"}:
+    # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
+    if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
         return None
 
     def joinuser(*args):
diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py
index 07ab27c7fb0c35..70bdecf2138fd9 100644
--- a/Lib/sysconfig/__init__.py
+++ b/Lib/sysconfig/__init__.py
@@ -21,6 +21,7 @@
 
 # Keys for get_config_var() that are never converted to Python integers.
 _ALWAYS_STR = {
+    'IPHONEOS_DEPLOYMENT_TARGET',
     'MACOSX_DEPLOYMENT_TARGET',
 }
 
@@ -57,6 +58,7 @@
         'scripts': '{base}/Scripts',
         'data': '{base}',
         },
+
     # Downstream distributors can overwrite the default install scheme.
     # This is done to support downstream modifications where distributors change
     # the installation layout (eg. different site-packages directory).
@@ -114,8 +116,8 @@ def _getuserbase():
     if env_base:
         return env_base
 
-    # Emscripten, VxWorks, and WASI have no home directories
-    if sys.platform in {"emscripten", "vxworks", "wasi"}:
+    # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
+    if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
         return None
 
     def joinuser(*args):
@@ -290,6 +292,7 @@ def _get_preferred_schemes():
             'home': 'posix_home',
             'user': 'osx_framework_user',
         }
+
     return {
         'prefix': 'posix_prefix',
         'home': 'posix_home',
@@ -623,10 +626,15 @@ def get_platform():
         if m:
             release = m.group()
     elif osname[:6] == "darwin":
-        import _osx_support
-        osname, release, machine = _osx_support.get_platform_osx(
-                                            get_config_vars(),
-                                            osname, release, machine)
+        if sys.platform == "ios":
+            release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "12.0")
+            osname = sys.platform
+            machine = sys.implementation._multiarch
+        else:
+            import _osx_support
+            osname, release, machine = _osx_support.get_platform_osx(
+                                                get_config_vars(),
+                                                osname, release, machine)
 
     return f"{osname}-{release}-{machine}"
 
diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py
index 5612c55746a516..c8bf16dd4d0d49 100644
--- a/Lib/test/pythoninfo.py
+++ b/Lib/test/pythoninfo.py
@@ -290,6 +290,7 @@ def format_groups(groups):
         "HOMEDRIVE",
         "HOMEPATH",
         "IDLESTARTUP",
+        "IPHONEOS_DEPLOYMENT_TARGET",
         "LANG",
         "LDFLAGS",
         "LDSHARED",
diff --git a/Lib/test/test_concurrent_futures/test_thread_pool.py b/Lib/test/test_concurrent_futures/test_thread_pool.py
index 5926a632aa4bec..16043fd1235614 100644
--- a/Lib/test/test_concurrent_futures/test_thread_pool.py
+++ b/Lib/test/test_concurrent_futures/test_thread_pool.py
@@ -49,6 +49,7 @@ def test_idle_thread_reuse(self):
         self.assertEqual(len(executor._threads), 1)
         executor.shutdown(wait=True)
 
+    @support.requires_fork()
     @unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork')
     @support.requires_resource('cpu')
     def test_hang_global_shutdown_lock(self):
diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py
index 3bf5c9ed41ee44..fa8e50fccb2c7b 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -1223,6 +1223,7 @@ def test_collect_garbage(self):
         self.assertEqual(len(gc.garbage), 0)
 
 
+    @requires_subprocess()
     @unittest.skipIf(BUILD_WITH_NDEBUG,
                      'built with -NDEBUG')
     def test_refcount_errors(self):
diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py
index 57f27b247d9d15..40d5fb338ce563 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -10,6 +10,14 @@
 from test import support
 from test.support import os_helper
 
+try:
+    # Some of the iOS tests need ctypes to operate.
+    # Confirm that the ctypes module is available
+    # is available.
+    import _ctypes
+except ImportError:
+    _ctypes = None
+
 FEDORA_OS_RELEASE = """\
 NAME=Fedora
 VERSION="32 (Thirty Two)"
@@ -228,10 +236,21 @@ def test_uname(self):
             if sys.platform == "android":
                 self.assertEqual(res.system, "Android")
                 self.assertEqual(res.release, platform.android_ver().release)
+            elif sys.platform == "ios":
+                # Platform module needs ctypes for full operation. If ctypes
+                # isn't available, there's no ObjC module, and dummy values are
+                # returned.
+                if _ctypes:
+                    self.assertIn(res.system, {"iOS", "iPadOS"})
+                    self.assertEqual(res.release, platform.ios_ver().release)
+                else:
+                    self.assertEqual(res.system, "")
+                    self.assertEqual(res.release, "")
             else:
                 self.assertEqual(res.system, uname.sysname)
                 self.assertEqual(res.release, uname.release)
 
+
     @unittest.skipUnless(sys.platform.startswith('win'), "windows only test")
     def test_uname_win32_without_wmi(self):
         def raises_oserror(*a):
@@ -422,6 +441,56 @@ def test_mac_ver_with_fork(self):
             # parent
             support.wait_process(pid, exitcode=0)
 
+    def test_ios_ver(self):
+        result = platform.ios_ver()
+
+        # ios_ver is only fully available on iOS where ctypes is available.
+        if sys.platform == "ios" and _ctypes:
+            system, release, model, is_simulator = result
+            # Result is a namedtuple
+            self.assertEqual(result.system, system)
+            self.assertEqual(result.release, release)
+            self.assertEqual(result.model, model)
+            self.assertEqual(result.is_simulator, is_simulator)
+
+            # We can't assert specific values without reproducing the logic of
+            # ios_ver(), so we check that the values are broadly what we expect.
+
+            # System is either iOS or iPadOS, depending on the test device
+            self.assertIn(system, {"iOS", "iPadOS"})
+
+            # Release is a numeric version specifier with at least 2 parts
+            parts = release.split(".")
+            self.assertGreaterEqual(len(parts), 2)
+            self.assertTrue(all(part.isdigit() for part in parts))
+
+            # If this is a simulator, we get a high level device descriptor
+            # with no identifying model number. If this is a physical device,
+            # we get a model descriptor like "iPhone13,1"
+            if is_simulator:
+                self.assertIn(model, {"iPhone", "iPad"})
+            else:
+                self.assertTrue(
+                    (model.startswith("iPhone") or model.startswith("iPad"))
+                    and "," in model
+                )
+
+            self.assertEqual(type(is_simulator), bool)
+        else:
+            # On non-iOS platforms, calling ios_ver doesn't fail; you get
+            # default values
+            self.assertEqual(result.system, "")
+            self.assertEqual(result.release, "")
+            self.assertEqual(result.model, "")
+            self.assertFalse(result.is_simulator)
+
+            # Check the fallback values can be overridden by arguments
+            override = platform.ios_ver("Foo", "Bar", "Whiz", True)
+            self.assertEqual(override.system, "Foo")
+            self.assertEqual(override.release, "Bar")
+            self.assertEqual(override.model, "Whiz")
+            self.assertTrue(override.is_simulator)
+
     @unittest.skipIf(support.is_emscripten, "Does not apply to Emscripten")
     def test_libc_ver(self):
         # check that libc_ver(executable) doesn't raise an exception
diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py
index c8315bbc8b727d..61c6a5a42502e7 100644
--- a/Lib/test/test_sysconfig.py
+++ b/Lib/test/test_sysconfig.py
@@ -8,7 +8,11 @@
 from copy import copy
 
 from test.support import (
-    captured_stdout, PythonSymlink, requires_subprocess, is_wasi
+    captured_stdout,
+    is_apple_mobile,
+    is_wasi,
+    PythonSymlink,
+    requires_subprocess,
 )
 from test.support.import_helper import import_module
 from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink,
@@ -346,6 +350,8 @@ def test_get_platform(self):
         # XXX more platforms to tests here
 
     @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
+    @unittest.skipIf(is_apple_mobile,
+                     f"{sys.platform} doesn't distribute header files in the runtime environment")
     def test_get_config_h_filename(self):
         config_h = sysconfig.get_config_h_filename()
         self.assertTrue(os.path.isfile(config_h), config_h)
@@ -423,6 +429,9 @@ def test_library(self):
             self.assertTrue(library.startswith(f'python{major}{minor}'))
             self.assertTrue(library.endswith('.dll'))
             self.assertEqual(library, ldlibrary)
+        elif is_apple_mobile:
+            framework = sysconfig.get_config_var('PYTHONFRAMEWORK')
+            self.assertEqual(ldlibrary, f"{framework}.framework/{framework}")
         else:
             self.assertTrue(library.startswith(f'libpython{major}.{minor}'))
             self.assertTrue(library.endswith('.a'))
@@ -476,6 +485,8 @@ def test_platform_in_subprocess(self):
         self.assertEqual(my_platform, test_platform)
 
     @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
+    @unittest.skipIf(is_apple_mobile,
+                     f"{sys.platform} doesn't include config folder at runtime")
     def test_srcdir(self):
         # See Issues #15322, #15364.
         srcdir = sysconfig.get_config_var('srcdir')
@@ -556,6 +567,8 @@ class MakefileTests(unittest.TestCase):
     @unittest.skipIf(sys.platform.startswith('win'),
                      'Test is not Windows compatible')
     @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
+    @unittest.skipIf(is_apple_mobile,
+                     f"{sys.platform} doesn't include config folder at runtime")
     def test_get_makefile_filename(self):
         makefile = sysconfig.get_makefile_filename()
         self.assertTrue(os.path.isfile(makefile), makefile)
diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
index 8c074cb28a87e3..a1bccb5f19b60f 100644
--- a/Lib/test/test_webbrowser.py
+++ b/Lib/test/test_webbrowser.py
@@ -5,11 +5,14 @@
 import subprocess
 from unittest import mock
 from test import support
+from test.support import is_apple_mobile
 from test.support import import_helper
 from test.support import os_helper
+from test.support import requires_subprocess
+from test.support import threading_helper
 
-if not support.has_subprocess_support:
-    raise unittest.SkipTest("test webserver requires subprocess")
+# The webbrowser module uses threading locks
+threading_helper.requires_working_threading(module=True)
 
 URL = 'https://www.example.com'
 CMD_NAME = 'test'
@@ -24,6 +27,7 @@ def wait(self, seconds=None):
         return 0
 
 
+@requires_subprocess()
 class CommandTestMixin:
 
     def _test(self, meth, *, args=[URL], kw={}, options, arguments):
@@ -219,6 +223,73 @@ def test_open_new_tab(self):
                    arguments=['openURL({},new-tab)'.format(URL)])
 
 
+@unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS")
+class IOSBrowserTest(unittest.TestCase):
+    def _obj_ref(self, *args):
+        # Construct a string representation of the arguments that can be used
+        # as a proxy for object instance references
+        return "|".join(str(a) for a in args)
+
+    @unittest.skipIf(getattr(webbrowser, "objc", None) is None,
+                     "iOS Webbrowser tests require ctypes")
+    def setUp(self):
+        # Intercept the the objc library. Wrap the calls to get the
+        # references to classes and selectors to return strings, and
+        # wrap msgSend to return stringified object references
+        self.orig_objc = webbrowser.objc
+
+        webbrowser.objc = mock.Mock()
+        webbrowser.objc.objc_getClass = lambda cls: f"C#{cls.decode()}"
+        webbrowser.objc.sel_registerName = lambda sel: f"S#{sel.decode()}"
+        webbrowser.objc.objc_msgSend.side_effect = self._obj_ref
+
+    def tearDown(self):
+        webbrowser.objc = self.orig_objc
+
+    def _test(self, meth, **kwargs):
+        # The browser always gets focus, there's no concept of separate browser
+        # windows, and there's no API-level control over creating a new tab.
+        # Therefore, all calls to webbrowser are effectively the same.
+        getattr(webbrowser, meth)(URL, **kwargs)
+
+        # The ObjC String version of the URL is created with UTF-8 encoding
+        url_string_args = [
+            "C#NSString",
+            "S#stringWithCString:encoding:",
+            b'https://www.example.com',
+            4,
+        ]
+        # The NSURL version of the URL is created from that string
+        url_obj_args = [
+            "C#NSURL",
+            "S#URLWithString:",
+            self._obj_ref(*url_string_args),
+        ]
+        # The openURL call is invoked on the shared application
+        shared_app_args = ["C#UIApplication", "S#sharedApplication"]
+
+        # Verify that the last call is the one that opens the URL.
+        webbrowser.objc.objc_msgSend.assert_called_with(
+            self._obj_ref(*shared_app_args),
+            "S#openURL:options:completionHandler:",
+            self._obj_ref(*url_obj_args),
+            None,
+            None
+        )
+
+    def test_open(self):
+        self._test('open')
+
+    def test_open_with_autoraise_false(self):
+        self._test('open', autoraise=False)
+
+    def test_open_new(self):
+        self._test('open_new')
+
+    def test_open_new_tab(self):
+        self._test('open_new_tab')
+
+
 class BrowserRegistrationTest(unittest.TestCase):
 
     def setUp(self):
@@ -314,6 +385,10 @@ def test_synthesize(self):
         webbrowser.register(name, None, webbrowser.GenericBrowser(name))
         webbrowser.get(sys.executable)
 
+    @unittest.skipIf(
+        is_apple_mobile,
+        "Apple mobile doesn't allow modifying browser with environment"
+    )
     def test_environment(self):
         webbrowser = import_helper.import_fresh_module('webbrowser')
         try:
@@ -325,6 +400,10 @@ def test_environment(self):
             webbrowser = import_helper.import_fresh_module('webbrowser')
             webbrowser.get()
 
+    @unittest.skipIf(
+        is_apple_mobile,
+        "Apple mobile doesn't allow modifying browser with environment"
+    )
     def test_environment_preferred(self):
         webbrowser = import_helper.import_fresh_module('webbrowser')
         try:
diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
index 0424c53b7ccaf9..7ef80a8f5ace9e 100755
--- a/Lib/webbrowser.py
+++ b/Lib/webbrowser.py
@@ -478,6 +478,9 @@ def register_standard_browsers():
         # OS X can use below Unix support (but we prefer using the OS X
         # specific stuff)
 
+    if sys.platform == "ios":
+        register("iosbrowser", None, IOSBrowser(), preferred=True)
+
     if sys.platform == "serenityos":
         # SerenityOS webbrowser, simply called "Browser".
         register("Browser", None, BackgroundBrowser("Browser"))
@@ -599,6 +602,70 @@ def open(self, url, new=0, autoraise=True):
             rc = osapipe.close()
             return not rc
 
+#
+# Platform support for iOS
+#
+if sys.platform == "ios":
+    from _ios_support import objc
+    if objc:
+        # If objc exists, we know ctypes is also importable.
+        from ctypes import c_void_p, c_char_p, c_ulong
+
+    class IOSBrowser(BaseBrowser):
+        def open(self, url, new=0, autoraise=True):
+            sys.audit("webbrowser.open", url)
+            # If ctypes isn't available, we can't open a browser
+            if objc is None:
+                return False
+
+            # All the messages in this call return object references.
+            objc.objc_msgSend.restype = c_void_p
+
+            # This is the equivalent of:
+            #    NSString url_string =
+            #        [NSString stringWithCString:url.encode("utf-8")
+            #                           encoding:NSUTF8StringEncoding];
+            NSString = objc.objc_getClass(b"NSString")
+            constructor = objc.sel_registerName(b"stringWithCString:encoding:")
+            objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_char_p, c_ulong]
+            url_string = objc.objc_msgSend(
+                NSString,
+                constructor,
+                url.encode("utf-8"),
+                4,  # NSUTF8StringEncoding = 4
+            )
+
+            # Create an NSURL object representing the URL
+            # This is the equivalent of:
+            #   NSURL *nsurl = [NSURL URLWithString:url];
+            NSURL = objc.objc_getClass(b"NSURL")
+            urlWithString_ = objc.sel_registerName(b"URLWithString:")
+            objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_void_p]
+            ns_url = objc.objc_msgSend(NSURL, urlWithString_, url_string)
+
+            # Get the shared UIApplication instance
+            # This code is the equivalent of:
+            # UIApplication shared_app = [UIApplication sharedApplication]
+            UIApplication = objc.objc_getClass(b"UIApplication")
+            sharedApplication = objc.sel_registerName(b"sharedApplication")
+            objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
+            shared_app = objc.objc_msgSend(UIApplication, sharedApplication)
+
+            # Open the URL on the shared application
+            # This code is the equivalent of:
+            #   [shared_app openURL:ns_url
+            #               options:NIL
+            #     completionHandler:NIL];
+            openURL_ = objc.sel_registerName(b"openURL:options:completionHandler:")
+            objc.objc_msgSend.argtypes = [
+                c_void_p, c_void_p, c_void_p, c_void_p, c_void_p
+            ]
+            # Method returns void
+            objc.objc_msgSend.restype = None
+            objc.objc_msgSend(shared_app, openURL_, ns_url, None, None)
+
+            return True
+
 
 def main():
     import getopt
diff --git a/Makefile.pre.in b/Makefile.pre.in
index c454f31aae1e57..5b89d6ba1acf71 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -186,12 +186,18 @@ PYTHONFRAMEWORKPREFIX=	@PYTHONFRAMEWORKPREFIX@
 PYTHONFRAMEWORKINSTALLDIR= @PYTHONFRAMEWORKINSTALLDIR@
 PYTHONFRAMEWORKINSTALLNAMEPREFIX= @PYTHONFRAMEWORKINSTALLNAMEPREFIX@
 RESSRCDIR= @RESSRCDIR@
-# Deployment target selected during configure, to be checked
+# macOS deployment target selected during configure, to be checked
 # by distutils. The export statement is needed to ensure that the
 # deployment target is active during build.
 MACOSX_DEPLOYMENT_TARGET=@CONFIGURE_MACOSX_DEPLOYMENT_TARGET@
 @EXPORT_MACOSX_DEPLOYMENT_TARGET@export MACOSX_DEPLOYMENT_TARGET
 
+# iOS Deployment target selected during configure. Unlike macOS, the iOS
+# deployment target is controlled using `-mios-version-min` arguments added to
+# CFLAGS and LDFLAGS by the configure script. This variable is not used during
+# the build, and is only listed here so it will be included in sysconfigdata.
+IPHONEOS_DEPLOYMENT_TARGET=@IPHONEOS_DEPLOYMENT_TARGET@
+
 # Option to install to strip binaries
 STRIPFLAG=-s
 
@@ -2038,11 +2044,23 @@ testios:
 	cp -r $(srcdir)/iOS/testbed $(XCFOLDER)
 	# Copy the framework from the install location to the testbed project.
 	cp -r $(PYTHONFRAMEWORKPREFIX)/* $(XCFOLDER)/Python.xcframework/ios-arm64_x86_64-simulator
+
 	# Run the test suite for the Xcode project, targeting the iOS simulator.
-	# If the suite fails, extract and print the console output, then re-raise the failure
+	# If the suite fails, touch a file in the test folder as a marker
 	if ! xcodebuild test -project $(XCFOLDER)/iOSTestbed.xcodeproj -scheme "iOSTestbed" -destination "platform=iOS Simulator,name=iPhone SE (3rd Generation)" -resultBundlePath $(XCRESULT) ; then \
-		xcrun xcresulttool get --path $(XCRESULT) --id $$(xcrun xcresulttool get --path $(XCRESULT) --format json | $(PYTHON_FOR_BUILD) -c "import sys, json; result = json.load(sys.stdin); print(result['actions']['_values'][0]['actionResult']['logRef']['id']['_value'])"); \
-		echo ; \
+	 	touch $(XCFOLDER)/failed; \
+	fi
+
+	# Regardless of success or failure, extract and print the test output
+	xcrun xcresulttool get --path $(XCRESULT) \
+		--id $$( \
+			xcrun xcresulttool get --path $(XCRESULT) --format json | \
+			$(PYTHON_FOR_BUILD) -c "import sys, json; result = json.load(sys.stdin); print(result['actions']['_values'][0]['actionResult']['logRef']['id']['_value'])" \
+		) \
+		--format json | \
+		$(PYTHON_FOR_BUILD) -c "import sys, json; result = json.load(sys.stdin); print(result['subsections']['_values'][1]['subsections']['_values'][0]['emittedOutput']['_value'])"
+
+	@if test -e $(XCFOLDER)/failed ; then \
 		exit 1; \
 	fi
 
@@ -2777,8 +2795,8 @@ frameworkinstallmobileheaders: frameworkinstallunversionedstructure inclinstall
 		echo "Removing old framework headers"; \
 		rm -rf $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers; \
 	fi
-	mv "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(VERSION)" "$(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers"
-	$(LN) -fs "../$(PYTHONFRAMEWORKDIR)/Headers" "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(VERSION)"
+	mv "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(LDVERSION)" "$(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers"
+	$(LN) -fs "../$(PYTHONFRAMEWORKDIR)/Headers" "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(LDVERSION)"
 
 # Build the toplevel Makefile
 Makefile.pre: $(srcdir)/Makefile.pre.in config.status
diff --git a/Misc/NEWS.d/next/Library/2024-03-19-14-35-57.gh-issue-114099.siNSpK.rst b/Misc/NEWS.d/next/Library/2024-03-19-14-35-57.gh-issue-114099.siNSpK.rst
new file mode 100644
index 00000000000000..9b57cbb812db4a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-03-19-14-35-57.gh-issue-114099.siNSpK.rst
@@ -0,0 +1 @@
+Modify standard library to allow for iOS platform differences.
diff --git a/Python/marshal.c b/Python/marshal.c
index daec7415b3fc7e..21d242bbb9757e 100644
--- a/Python/marshal.c
+++ b/Python/marshal.c
@@ -14,6 +14,10 @@
 #include "pycore_setobject.h"     // _PySet_NextEntry()
 #include "marshal.h"              // Py_MARSHAL_VERSION
 
+#ifdef __APPLE__
+#  include "TargetConditionals.h"
+#endif /* __APPLE__ */
+
 /*[clinic input]
 module marshal
 [clinic start generated code]*/
@@ -33,11 +37,14 @@ module marshal
  * #if defined(MS_WINDOWS) && defined(_DEBUG)
  */
 #if defined(MS_WINDOWS)
-#define MAX_MARSHAL_STACK_DEPTH 1000
+#  define MAX_MARSHAL_STACK_DEPTH 1000
 #elif defined(__wasi__)
-#define MAX_MARSHAL_STACK_DEPTH 1500
+#  define MAX_MARSHAL_STACK_DEPTH 1500
+// TARGET_OS_IPHONE covers any non-macOS Apple platform.
+#elif defined(__APPLE__) && TARGET_OS_IPHONE
+#  define MAX_MARSHAL_STACK_DEPTH 1500
 #else
-#define MAX_MARSHAL_STACK_DEPTH 2000
+#  define MAX_MARSHAL_STACK_DEPTH 2000
 #endif
 
 #define TYPE_NULL               '0'
diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h
index 2445a5c838a7d7..ac9d91b5e12885 100644
--- a/Python/stdlib_module_names.h
+++ b/Python/stdlib_module_names.h
@@ -38,6 +38,7 @@ static const char* _Py_stdlib_module_names[] = {
 "_heapq",
 "_imp",
 "_io",
+"_ios_support",
 "_json",
 "_locale",
 "_lsprof",
diff --git a/configure b/configure
index 0493d382f48481..542783e723d934 100755
--- a/configure
+++ b/configure
@@ -976,7 +976,7 @@ LDFLAGS
 CFLAGS
 CC
 HAS_XCRUN
-IOS_DEPLOYMENT_TARGET
+IPHONEOS_DEPLOYMENT_TARGET
 EXPORT_MACOSX_DEPLOYMENT_TARGET
 CONFIGURE_MACOSX_DEPLOYMENT_TARGET
 _PYTHON_HOST_PLATFORM
@@ -4442,15 +4442,16 @@ if test "$cross_compiling" = yes; then
 		_host_device=`echo $host | cut -d '-' -f4`
 		_host_device=${_host_device:=os}
 
-				IOS_DEPLOYMENT_TARGET=${_host_os:3}
-		IOS_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET:=12.0}
+		# IPHONEOS_DEPLOYMENT_TARGET is the minimum supported iOS version
+		IPHONEOS_DEPLOYMENT_TARGET=${_host_os:3}
+		IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:=12.0}
 
 		case "$host_cpu" in
 			aarch64)
-				_host_ident=${IOS_DEPLOYMENT_TARGET}-arm64-iphone${_host_device}
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-arm64-iphone${_host_device}
 				;;
 			*)
-				_host_ident=${IOS_DEPLOYMENT_TARGET}-$host_cpu-iphone${_host_device}
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-$host_cpu-iphone${_host_device}
 				;;
 		esac
 		;;
@@ -4597,6 +4598,9 @@ fi
 CONFIGURE_MACOSX_DEPLOYMENT_TARGET=
 EXPORT_MACOSX_DEPLOYMENT_TARGET='#'
 
+# Record the value of IPHONEOS_DEPLOYMENT_TARGET enforced by the selected host triple.
+
+
 # checks for alternative programs
 
 # compiler flags are generated in two sets, BASECFLAGS and OPT.  OPT is just
@@ -4632,9 +4636,8 @@ esac
 case $ac_sys_system in #(
   iOS) :
 
-    as_fn_append CFLAGS " -mios-version-min=${IOS_DEPLOYMENT_TARGET}"
-    as_fn_append LDFLAGS " -mios-version-min=${IOS_DEPLOYMENT_TARGET}"
-
+    as_fn_append CFLAGS " -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"
+    as_fn_append LDFLAGS " -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"
    ;; #(
   *) :
      ;;
@@ -27497,6 +27500,8 @@ else $as_nop
     with_ensurepip=no ;; #(
   WASI) :
     with_ensurepip=no ;; #(
+  iOS) :
+    with_ensurepip=no ;; #(
   *) :
     with_ensurepip=upgrade
        ;;
diff --git a/configure.ac b/configure.ac
index cdfafc2d7272f1..fc62bfe5a1d4c4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -715,16 +715,16 @@ if test "$cross_compiling" = yes; then
 		_host_device=`echo $host | cut -d '-' -f4`
 		_host_device=${_host_device:=os}
 
-		dnl IOS_DEPLOYMENT_TARGET is the minimum supported iOS version
-		IOS_DEPLOYMENT_TARGET=${_host_os:3}
-		IOS_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET:=12.0}
+		# IPHONEOS_DEPLOYMENT_TARGET is the minimum supported iOS version
+		IPHONEOS_DEPLOYMENT_TARGET=${_host_os:3}
+		IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:=12.0}
 
 		case "$host_cpu" in
 			aarch64)
-				_host_ident=${IOS_DEPLOYMENT_TARGET}-arm64-iphone${_host_device}
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-arm64-iphone${_host_device}
 				;;
 			*)
-				_host_ident=${IOS_DEPLOYMENT_TARGET}-$host_cpu-iphone${_host_device}
+				_host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-$host_cpu-iphone${_host_device}
 				;;
 		esac
 		;;
@@ -866,6 +866,9 @@ AC_SUBST([EXPORT_MACOSX_DEPLOYMENT_TARGET])
 CONFIGURE_MACOSX_DEPLOYMENT_TARGET=
 EXPORT_MACOSX_DEPLOYMENT_TARGET='#'
 
+# Record the value of IPHONEOS_DEPLOYMENT_TARGET enforced by the selected host triple.
+AC_SUBST([IPHONEOS_DEPLOYMENT_TARGET])
+
 # checks for alternative programs
 
 # compiler flags are generated in two sets, BASECFLAGS and OPT.  OPT is just
@@ -901,9 +904,8 @@ AS_CASE([$host],
 dnl Add the compiler flag for the iOS minimum supported OS version.
 AS_CASE([$ac_sys_system],
   [iOS], [
-    AS_VAR_APPEND([CFLAGS], [" -mios-version-min=${IOS_DEPLOYMENT_TARGET}"])
-    AS_VAR_APPEND([LDFLAGS], [" -mios-version-min=${IOS_DEPLOYMENT_TARGET}"])
-    AC_SUBST([IOS_DEPLOYMENT_TARGET])
+    AS_VAR_APPEND([CFLAGS], [" -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"])
+    AS_VAR_APPEND([LDFLAGS], [" -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"])
   ],
 )
 
@@ -6939,6 +6941,7 @@ AC_ARG_WITH([ensurepip],
       AS_CASE([$ac_sys_system],
         [Emscripten], [with_ensurepip=no],
         [WASI], [with_ensurepip=no],
+        [iOS], [with_ensurepip=no],
         [with_ensurepip=upgrade]
       )
     ])
diff --git a/iOS/Resources/Info.plist.in b/iOS/Resources/Info.plist.in
index 52c0a6e7fd7a55..c3e261ecd9eff7 100644
--- a/iOS/Resources/Info.plist.in
+++ b/iOS/Resources/Info.plist.in
@@ -29,6 +29,6 @@
 		<string>iPhoneOS</string>
 	</array>
 	<key>MinimumOSVersion</key>
-	<string>@IOS_DEPLOYMENT_TARGET@</string>
+	<string>@IPHONEOS_DEPLOYMENT_TARGET@</string>
 </dict>
 </plist>
diff --git a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj
index 4389c08ac1960d..d57cfc3dbe0304 100644
--- a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj
+++ b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj
@@ -441,7 +441,7 @@
 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
-				DEVELOPMENT_TEAM = 3HEZE76D99;
+				DEVELOPMENT_TEAM = "";
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
 				INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist";
@@ -471,7 +471,7 @@
 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
-				DEVELOPMENT_TEAM = 3HEZE76D99;
+				DEVELOPMENT_TEAM = "";
 				ENABLE_TESTABILITY = YES;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";