Repository: encode/django-rest-framework

Issue:
`validators` parameter in field cannot deal with `tuple`
## Checklist

- [X] I have verified that that issue exists against the `master` branch of Django REST framework.
- [X] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [X] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [X] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.)
- [X] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

## Steps to reproduce
If `tuple` validators (e.g., `validators=(1, 2, 3)`) is passed to field rather than `list`, DRF crashed, as it assumes `validators` is `list`.

## Expected behavior
Django actually uses `tuple` as default type for `validators`, and converts it internally to `list`:
https://github.com/django/django/commit/9027e6c8a3711034d345535036d1a276d9a8b829

This approach takes care of both `tuple` and `list` types for `validators`.

## Actual behavior
However, DRF assumes `validators` is `list`.  Though it also uses `self.validators = validators[:]` to generate a new object, it failed to deal with `tuple`.

Hence, in recent change of DRF 3.9.0, the new stuff would break any code passing `tuple` to `validators`:
https://github.com/encode/django-rest-framework/commit/0eb2dc1137189027cc8d638630fb1754b02d6cfa#diff-af402f515db75c7c6754453cf15455d9

The `append()` approach was there in the past, but usually not triggered by default.  Now due to the new `ProhibitNullCharactersValidator`, such behavior becomes more popular.

I'd suggest a similar approach to that of Django (`tuple` by default, and convert to `list` internally), so both types could be accepted and taken care of.

## PS
Let's discuss if this is the desired approach.  If so, and if no one would like to fix it ASAP, I'd be glad to submit a PR, as my code is not working with DRF 3.9.0.


Discussion:
Being consistent with Django's behavior would be the correct thing to do here. 

Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -350,7 +350,7 @@ def __init__(self, read_only=False, write_only=False,
                 self.default_empty_html = default
 
         if validators is not None:
-            self.validators = validators[:]
+            self.validators = list(validators)
 
         # These are set up by `.bind()` when the field is added to a serializer.
         self.field_name = None
@@ -410,7 +410,7 @@ def validators(self, validators):
         self._validators = validators
 
     def get_validators(self):
-        return self.default_validators[:]
+        return list(self.default_validators)
 
     def get_initial(self):
         """
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -393,7 +393,7 @@ def get_validators(self):
         # Used by the lazily-evaluated `validators` property.
         meta = getattr(self, 'Meta', None)
         validators = getattr(meta, 'validators', None)
-        return validators[:] if validators else []
+        return list(validators) if validators else []
 
     def get_initial(self):
         if hasattr(self, 'initial_data'):
@@ -1480,7 +1480,7 @@ def get_validators(self):
         # If the validators have been declared explicitly then use that.
         validators = getattr(getattr(self, 'Meta', None), 'validators', None)
         if validators is not None:
-            return validators[:]
+            return list(validators)
 
         # Otherwise use the default set of validators.
         return (


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -740,6 +740,25 @@ def test_null_bytes(self):
                 'Null characters are not allowed.'
             ]
 
+    def test_iterable_validators(self):
+        """
+        Ensure `validators` parameter is compatible with reasonable iterables.
+        """
+        value = 'example'
+
+        for validators in ([], (), set()):
+            field = serializers.CharField(validators=validators)
+            field.run_validation(value)
+
+        def raise_exception(value):
+            raise exceptions.ValidationError('Raised error')
+
+        for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
+            field = serializers.CharField(validators=validators)
+            with pytest.raises(serializers.ValidationError) as exc_info:
+                field.run_validation(value)
+            assert exc_info.value.detail == ['Raised error']
+
 
 class TestEmailField(FieldValues):
     """
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -10,7 +10,7 @@
 import pytest
 from django.db import models
 
-from rest_framework import fields, relations, serializers
+from rest_framework import exceptions, fields, relations, serializers
 from rest_framework.compat import unicode_repr
 from rest_framework.fields import Field
 
@@ -183,6 +183,38 @@ def to_internal_value(self, data):
         assert serializer.validated_data.coords[1] == 50.941357
         assert serializer.errors == {}
 
+    def test_iterable_validators(self):
+        """
+        Ensure `validators` parameter is compatible with reasonable iterables.
+        """
+        data = {'char': 'abc', 'integer': 123}
+
+        for validators in ([], (), set()):
+            class ExampleSerializer(serializers.Serializer):
+                char = serializers.CharField(validators=validators)
+                integer = serializers.IntegerField()
+
+            serializer = ExampleSerializer(data=data)
+            assert serializer.is_valid()
+            assert serializer.validated_data == data
+            assert serializer.errors == {}
+
+        def raise_exception(value):
+            raise exceptions.ValidationError('Raised error')
+
+        for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
+            class ExampleSerializer(serializers.Serializer):
+                char = serializers.CharField(validators=validators)
+                integer = serializers.IntegerField()
+
+            serializer = ExampleSerializer(data=data)
+            assert not serializer.is_valid()
+            assert serializer.data == data
+            assert serializer.validated_data == {}
+            assert serializer.errors == {'char': [
+                exceptions.ErrorDetail(string='Raised error', code='invalid')
+            ]}
+
 
 class TestValidateMethod:
     def test_non_field_error_validate_method(self):