diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
index c71349a98106c5ca5df3d84a952369a5dc66bd3e..dfa042c142242110cc8b920743c32b67e486223f 100644
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -861,8 +861,9 @@ class CArgDeclNode(Node):
     # annotation     ExprNode or None   Py3 function arg annotation
     # is_self_arg    boolean            Is the "self" arg of an extension type method
     # is_type_arg    boolean            Is the "class" arg of an extension type classmethod
-    # is_kw_only     boolean            Is a keyword-only argument
+    # kw_only        boolean            Is a keyword-only argument
     # is_dynamic     boolean            Non-literal arg stored inside CyFunction
+    # pos_only       boolean            Is a positional-only argument
 
     child_attrs = ["base_type", "declarator", "default", "annotation"]
     outer_attrs = ["default", "annotation"]
@@ -871,6 +872,7 @@ class CArgDeclNode(Node):
     is_type_arg = 0
     is_generic = 1
     kw_only = 0
+    pos_only = 0
     not_none = 0
     or_none = 0
     type = None
@@ -3655,6 +3657,7 @@ class DefNodeWrapper(FuncDefNode):
         positional_args = []
         required_kw_only_args = []
         optional_kw_only_args = []
+        num_pos_only_args = 0
         for arg in args:
             if arg.is_generic:
                 if arg.default:
@@ -3668,6 +3671,9 @@ class DefNodeWrapper(FuncDefNode):
                 elif not arg.is_self_arg and not arg.is_type_arg:
                     positional_args.append(arg)
 
+                if arg.pos_only:
+                    num_pos_only_args += 1
+
         # sort required kw-only args before optional ones to avoid special
         # cases in the unpacking code
         kw_only_args = required_kw_only_args + optional_kw_only_args
@@ -3685,10 +3691,10 @@ class DefNodeWrapper(FuncDefNode):
 
         code.putln('{')
         all_args = tuple(positional_args) + tuple(kw_only_args)
-        code.putln("static PyObject **%s[] = {%s,0};" % (
+        code.putln("static PyObject **%s[] = {%s};" % (
             Naming.pykwdlist_cname,
             ','.join(['&%s' % code.intern_identifier(arg.name)
-                      for arg in all_args])))
+                      for arg in all_args if not arg.pos_only] + ['0'])))
 
         # Before being converted and assigned to the target variables,
         # borrowed references to all unpacked argument values are
@@ -3706,8 +3712,8 @@ class DefNodeWrapper(FuncDefNode):
             Naming.kwds_cname))
         self.generate_keyword_unpacking_code(
             min_positional_args, max_positional_args,
-            has_fixed_positional_count, has_kw_only_args,
-            all_args, argtuple_error_label, code)
+            num_pos_only_args, has_fixed_positional_count,
+            has_kw_only_args, all_args, argtuple_error_label, code)
 
         # --- optimised code when we do not receive any keyword arguments
         if (self.num_required_kw_args and min_positional_args > 0) or min_positional_args == max_positional_args:
@@ -3870,8 +3876,8 @@ class DefNodeWrapper(FuncDefNode):
                 code.putln('values[%d] = %s;' % (i, arg.type.as_pyobject(default_value)))
 
     def generate_keyword_unpacking_code(self, min_positional_args, max_positional_args,
-                                        has_fixed_positional_count, has_kw_only_args,
-                                        all_args, argtuple_error_label, code):
+                                        num_pos_only_args, has_fixed_positional_count,
+                                        has_kw_only_args, all_args, argtuple_error_label, code):
         code.putln('Py_ssize_t kw_args;')
         code.putln('const Py_ssize_t pos_args = PyTuple_GET_SIZE(%s);' % Naming.args_cname)
         # copy the values from the args tuple and check that it's not too long
@@ -3901,9 +3907,12 @@ class DefNodeWrapper(FuncDefNode):
         code.putln('kw_args = PyDict_Size(%s);' % Naming.kwds_cname)
         if self.num_required_args or max_positional_args > 0:
             last_required_arg = -1
+            last_required_posonly_arg = -1
             for i, arg in enumerate(all_args):
                 if not arg.default:
                     last_required_arg = i
+                if arg.pos_only and not arg.default:
+                    last_required_posonly_arg = i
             if last_required_arg < max_positional_args:
                 last_required_arg = max_positional_args-1
             if max_positional_args > 0:
@@ -3917,6 +3926,12 @@ class DefNodeWrapper(FuncDefNode):
                     else:
                         code.putln('case %2d:' % i)
                 pystring_cname = code.intern_identifier(arg.name)
+                if arg.pos_only:
+                    if i == last_required_posonly_arg:
+                        code.put_goto(argtuple_error_label)
+                    if i == last_required_arg:
+                        code.putln('break;')
+                    continue
                 if arg.default:
                     if arg.kw_only:
                         # optional kw-only args are handled separately below
@@ -3971,14 +3986,34 @@ class DefNodeWrapper(FuncDefNode):
         # arguments, this will always do the right thing for unpacking
         # keyword arguments, so that we can concentrate on optimising
         # common cases above.
+        #
+        # ParseOptionalKeywords() needs to know how many of the arguments
+        # that could be passed as keywords have in fact been passed as
+        # positional args.
+        if num_pos_only_args > 0:
+            # There are positional-only arguments which we don't want to count,
+            # since they cannot be keyword arguments.  Subtract the number of
+            # pos-only arguments from the number of positional arguments we got.
+            # If we get a negative number then none of the keyword arguments were
+            # passed as positional args.
+            code.putln('const Py_ssize_t kwd_pos_args = (pos_args < %d) ? 0 : (pos_args - %d);' % (
+                num_pos_only_args, num_pos_only_args))
+        elif max_positional_args > 0:
+            code.putln('const Py_ssize_t kwd_pos_args = pos_args;')
+
         if max_positional_args == 0:
             pos_arg_count = "0"
         elif self.star_arg:
-            code.putln("const Py_ssize_t used_pos_args = (pos_args < %d) ? pos_args : %d;" % (
-                max_positional_args, max_positional_args))
+            # If there is a *arg, the number of used positional args could be larger than
+            # the number of possible keyword arguments.  But ParseOptionalKeywords() uses the
+            # number of positional args as an index into the keyword argument name array,
+            # if this is larger than the number of kwd args we get a segfault.  So round
+            # this down to max_positional_args - num_pos_only_args (= num possible kwd args).
+            code.putln("const Py_ssize_t used_pos_args = (kwd_pos_args < %d) ? kwd_pos_args : %d;" % (
+                max_positional_args - num_pos_only_args, max_positional_args - num_pos_only_args))
             pos_arg_count = "used_pos_args"
         else:
-            pos_arg_count = "pos_args"
+            pos_arg_count = "kwd_pos_args"
         code.globalstate.use_utility_code(
             UtilityCode.load_cached("ParseKeywords", "FunctionArguments.c"))
         code.putln('if (unlikely(__Pyx_ParseOptionalKeywords(%s, %s, %s, values, %s, "%s") < 0)) %s' % (
diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py
index de3cff9aaf36af64d01491ed25d6f76fc685b301..d540a001da343a9b260f0be510c11ce2c15c5d53 100644
--- a/Cython/Compiler/Parsing.py
+++ b/Cython/Compiler/Parsing.py
@@ -2965,7 +2965,7 @@ def p_exception_value_clause(s):
             exc_val = p_test(s)
     return exc_val, exc_check
 
-c_arg_list_terminators = cython.declare(set, set(['*', '**', '.', ')', ':']))
+c_arg_list_terminators = cython.declare(set, set(['*', '**', '.', ')', ':', '/']))
 
 def p_c_arg_list(s, ctx = Ctx(), in_pyfunc = 0, cmethod_flag = 0,
                  nonempty_declarators = 0, kw_only = 0, annotated = 1):
@@ -3424,6 +3424,20 @@ def p_varargslist(s, terminator=')', annotated=1):
                         annotated = annotated)
     star_arg = None
     starstar_arg = None
+    if s.sy == '/':
+        if len(args) == 0:
+            s.error("Got zero positional-only arguments despite presence of "
+                    "positional-only specifier '/'")
+        s.next()
+        # Mark all args to the left as pos only
+        for arg in args:
+            arg.pos_only = 1
+        if s.sy == ',':
+            s.next()
+            args.extend(p_c_arg_list(s, in_pyfunc = 1,
+                nonempty_declarators = 1, annotated = annotated))
+        elif s.sy != terminator:
+            s.error("Syntax error in Python function argument list")
     if s.sy == '*':
         s.next()
         if s.sy == 'IDENT':
diff --git a/tests/compile/posonly.pyx b/tests/compile/posonly.pyx
new file mode 100644
index 0000000000000000000000000000000000000000..286d4390b3bcda69596febf1fc513804542cca18
--- /dev/null
+++ b/tests/compile/posonly.pyx
@@ -0,0 +1,17 @@
+# mode: compile
+# tag: posonly
+
+# TODO: remove posonly tag before merge (and maybe remove this test,
+#       since it seems covered by the runs/ test)
+
+def test(x, y, z=42, /, w=43):
+    pass
+
+def test2(x, y, /):
+    pass
+
+def test3(x, /, z):
+    pass
+
+def test4(x, /, z, *, w):
+    pass
diff --git a/tests/run/posonly.pyx b/tests/run/posonly.pyx
new file mode 100644
index 0000000000000000000000000000000000000000..50d3e77ed6c8e2faa4e1773903c16466eedce55f
--- /dev/null
+++ b/tests/run/posonly.pyx
@@ -0,0 +1,323 @@
+# mode: run
+# tag: posonly
+
+# TODO: remove posonly tag before merge
+
+import cython
+
+# TODO: add the test below to an 'error' test
+#def test_invalid_syntax_errors():
+#    def f(a, b = 5, /, c): pass
+#    def f(a = 5, b, /, c): pass
+#    def f(a = 5, b, /): pass
+#    def f(*args, /): pass
+#    def f(*args, a, /): pass
+#    def f(**kwargs, /): pass
+#    def f(/, a = 1): pass
+#    def f(/, a): pass
+#    def f(/): pass
+#    def f(*, a, /): pass
+#    def f(*, /, a): pass
+#    def f(a, /, a): pass
+#    def f(a, /, *, a): pass
+#    def f(a, b/2, c): pass
+
+def test_optional_posonly_args1(a, b=10, /, c=100):
+    """
+    >>> test_optional_posonly_args1(1, 2, 3)
+    6
+    >>> test_optional_posonly_args1(1, 2, c=3)
+    6
+    >>> test_optional_posonly_args1(1, b=2, c=3)
+    Traceback (most recent call last):
+    TypeError: test_optional_posonly_args1() got an unexpected keyword argument 'b'
+    >>> test_optional_posonly_args1(1, 2)
+    103
+    >>> test_optional_posonly_args1(1, b=2)
+    Traceback (most recent call last):
+    TypeError: test_optional_posonly_args1() got an unexpected keyword argument 'b'
+    """
+    return a + b + c
+
+def test_optional_posonly_args2(a=1, b=10, /, c=100):
+    """
+    >>> test_optional_posonly_args2(1, 2, 3)
+    6
+    >>> test_optional_posonly_args2(1, 2, c=3)
+    6
+    >>> test_optional_posonly_args2(1, b=2, c=3)
+    Traceback (most recent call last):
+    TypeError: test_optional_posonly_args2() got an unexpected keyword argument 'b'
+    >>> test_optional_posonly_args2(1, 2)
+    103
+    >>> test_optional_posonly_args2(1, b=2)
+    Traceback (most recent call last):
+    TypeError: test_optional_posonly_args2() got an unexpected keyword argument 'b'
+    >>> test_optional_posonly_args2(1, c=2)
+    13
+    """
+    return a + b + c
+
+# TODO: remove the test below?  would need to hard-code the function with > 255 posonly args
+#def test_syntax_for_many_positional_only():
+#    # more than 255 positional only arguments, should compile ok
+#    fundef = "def f(%s, /):\n  pass\n" % ', '.join('i%d' % i for i in range(300))
+#    compile(fundef, "<test>", "single")
+
+# TODO: remove the test below?  doesn't seem relevant to Cython implementation
+#def test_pos_only_definition(self):
+#    def f(a, b, c, /, d, e=1, *, f, g=2):
+#        pass
+#
+#    self.assertEqual(2, f.__code__.co_argcount)  # 2 "standard args"
+#    self.assertEqual(3, f.__code__.co_posonlyargcount)
+#    self.assertEqual((1,), f.__defaults__)
+#
+#    def f(a, b, c=1, /, d=2, e=3, *, f, g=4):
+#        pass
+#
+#    self.assertEqual(2, f.__code__.co_argcount)  # 2 "standard args"
+#    self.assertEqual(3, f.__code__.co_posonlyargcount)
+#    self.assertEqual((1, 2, 3), f.__defaults__)
+
+def test_pos_only_call_via_unpacking(a, b, /):
+    """
+    >>> test_pos_only_call_via_unpacking(*[1,2])
+    3
+    """
+    return a + b
+
+def test_use_positional_as_keyword1(a, /):
+    """
+    >>> test_use_positional_as_keyword1(a=1)
+    Traceback (most recent call last):
+    TypeError: test_use_positional_as_keyword1() takes no keyword arguments
+    """
+    pass
+
+def test_use_positional_as_keyword2(a, /, b):
+    """
+    >>> test_use_positional_as_keyword2(a=1, b=2)
+    Traceback (most recent call last):
+    TypeError: test_use_positional_as_keyword2() takes exactly 2 positional arguments (0 given)
+    """
+    pass
+
+def test_use_positional_as_keyword3(a, b, /):
+    """
+    >>> test_use_positional_as_keyword3(a=1, b=2)
+    Traceback (most recent call last):
+    TypeError: test_use_positional_as_keyword3() takes exactly 2 positional arguments (0 given)
+    """
+    pass
+
+def test_positional_only_and_arg_invalid_calls(a, b, /, c):
+    """
+    >>> test_positional_only_and_arg_invalid_calls(1, 2)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_and_arg_invalid_calls() takes exactly 3 positional arguments (2 given)
+    >>> test_positional_only_and_arg_invalid_calls(1)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_and_arg_invalid_calls() takes exactly 3 positional arguments (1 given)
+    >>> test_positional_only_and_arg_invalid_calls(1,2,3,4)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_and_arg_invalid_calls() takes exactly 3 positional arguments (4 given)
+    """
+    pass
+
+def test_positional_only_and_optional_arg_invalid_calls(a, b, /, c=3):
+    """
+    >>> test_positional_only_and_optional_arg_invalid_calls(1, 2)
+    >>> test_positional_only_and_optional_arg_invalid_calls(1)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_and_optional_arg_invalid_calls() takes at least 2 positional arguments (1 given)
+    >>> test_positional_only_and_optional_arg_invalid_calls()
+    Traceback (most recent call last):
+    TypeError: test_positional_only_and_optional_arg_invalid_calls() takes at least 2 positional arguments (0 given)
+    >>> test_positional_only_and_optional_arg_invalid_calls(1, 2, 3, 4)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_and_optional_arg_invalid_calls() takes at most 3 positional arguments (4 given)
+    """
+    pass
+
+def test_positional_only_invalid_calls(a, b, /):
+    """
+    >>> test_positional_only_invalid_calls(1, 2)
+    >>> test_positional_only_invalid_calls(1)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_invalid_calls() takes exactly 2 positional arguments (1 given)
+    >>> test_positional_only_invalid_calls()
+    Traceback (most recent call last):
+    TypeError: test_positional_only_invalid_calls() takes exactly 2 positional arguments (0 given)
+    >>> test_positional_only_invalid_calls(1, 2, 3)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_invalid_calls() takes exactly 2 positional arguments (3 given)
+    """
+    pass
+
+def test_positional_only_with_optional_invalid_calls(a, b=2, /):
+    """
+    >>> test_positional_only_with_optional_invalid_calls(1)
+    >>> test_positional_only_with_optional_invalid_calls()
+    Traceback (most recent call last):
+    TypeError: test_positional_only_with_optional_invalid_calls() takes at least 1 positional argument (0 given)
+    >>> test_positional_only_with_optional_invalid_calls(1, 2, 3)
+    Traceback (most recent call last):
+    TypeError: test_positional_only_with_optional_invalid_calls() takes at most 2 positional arguments (3 given)
+    """
+    pass
+
+def test_no_standard_args_usage(a, b, /, *, c):
+    """
+    >>> test_no_standard_args_usage(1, 2, c=3)
+    >>> test_no_standard_args_usage(1, b=2, c=3)
+    Traceback (most recent call last):
+    TypeError: test_no_standard_args_usage() takes exactly 2 positional arguments (1 given)
+    """
+    pass
+
+#def test_change_default_pos_only():
+# TODO: probably remove this, since we have no __defaults__ in Cython?
+#    """
+#    >>> test_change_default_pos_only()
+#    True
+#    True
+#    """
+#    def f(a, b=2, /, c=3):
+#        return a + b + c
+#
+#    print((2,3) == f.__defaults__)
+#    f.__defaults__ = (1, 2, 3)
+#    print(f(1, 2, 3) == 6)
+
+def test_lambdas():
+    """
+    >>> test_lambdas()
+    3
+    3
+    3
+    3
+    3
+    """
+    x = lambda a, /, b: a + b
+    print(x(1,2))
+    print(x(1,b=2))
+
+    x = lambda a, /, b=2: a + b
+    print(x(1))
+
+    x = lambda a, b, /: a + b
+    print(x(1, 2))
+
+    x = lambda a, b, /, : a + b
+    print(x(1, 2))
+
+
+#TODO: need to implement this in the 'error' test
+#def test_invalid_syntax_lambda(self):
+#    lambda a, b = 5, /, c: None
+#    lambda a = 5, b, /, c: None
+#    lambda a = 5, b, /: None
+#    lambda a, /, a: None
+#    lambda a, /, *, a: None
+#    lambda *args, /: None
+#    lambda *args, a, /: None
+#    lambda **kwargs, /: None
+#    lambda /, a = 1: None
+#    lambda /, a: None
+#    lambda /: None
+#    lambda *, a, /: None
+#    lambda *, /, a: None
+
+class Example:
+    def f(self, a, b, /):
+        return a, b
+
+def test_posonly_methods():
+    """
+    >>> Example().f(1,2)
+    (1, 2)
+    >>> Example.f(Example(), 1, 2)
+    (1, 2)
+    >>> try:
+    ...     Example.f(1,2)
+    ... except TypeError:
+    ...    print("Got type error")
+    Got type error
+    >>> Example().f(1, b=2)
+    Traceback (most recent call last):
+    TypeError: f() takes exactly 3 positional arguments (2 given)
+    """
+    pass
+
+class X:
+    def f(self, *, __a=42):
+        return __a
+def test_mangling():
+    """
+    >>> X().f()
+    42
+    """
+    pass
+
+def global_pos_only_f(a, b, /):
+    pass
+
+def test_module_function():
+    """
+    >>> global_pos_only_f()
+    Traceback (most recent call last):
+    TypeError: global_pos_only_f() takes exactly 2 positional arguments (0 given)
+    """
+    pass
+
+def test_closures1(x,y):
+    """
+    >>> test_closures1(1,2)(3,4)
+    10
+    >>> test_closures1(1,2)(3)
+    Traceback (most recent call last):
+    TypeError: g() takes exactly 2 positional arguments (1 given)
+    >>> test_closures1(1,2)(3,4,5)
+    Traceback (most recent call last):
+    TypeError: g() takes exactly 2 positional arguments (3 given)
+    """
+    def g(x2,/,y2):
+        return x + y + x2 + y2
+    return g
+
+def test_closures2(x,/,y):
+    """
+    >>> test_closures2(1,2)(3,4)
+    10
+    """
+    def g(x2,y2):
+        return x + y + x2 + y2
+    return g
+
+def test_closures3(x,/,y):
+    """
+    >>> test_closures3(1,2)(3,4)
+    10
+    >>> test_closures3(1,2)(3)
+    Traceback (most recent call last):
+    TypeError: g() takes exactly 2 positional arguments (1 given)
+    >>> test_closures3(1,2)(3,4,5)
+    Traceback (most recent call last):
+    TypeError: g() takes exactly 2 positional arguments (3 given)
+    """
+    def g(x2,/,y2):
+        return x + y + x2 + y2
+    return g
+
+def test_same_keyword_as_positional_with_kwargs(something, /, **kwargs):
+    """
+    >>> test_same_keyword_as_positional_with_kwargs(42, something=42)
+    (42, {'something': 42})
+    >>> test_same_keyword_as_positional_with_kwargs(something=42)
+    Traceback (most recent call last):
+    TypeError: test_same_keyword_as_positional_with_kwargs() takes exactly 1 positional argument (0 given)
+    >>> test_same_keyword_as_positional_with_kwargs(42)
+    (42, {})
+    """
+    return (something, kwargs)